Методичка по C (987695), страница 6
Текст из файла (страница 6)
Console.WriteLine("Elements of Array");
for (int i = 0; i < k.Length; i++)
Console.WriteLine("K[" + i + "]=" + k[i]);
} }
На его базе можно построить классы обработки массивов. В нашем случае – нахождение суммы. Класс-наследник включает все данные своего предка (за исключением данных с атрибутом доступа private). Наследуются по общим правилам и индексаторы, и свойства, а также методы перегрузки операторов.
class proc1 : arr // задаем базовый класс arr
{
int q;
public proc1()
{ // конструктор класса-наследника
Console.Write("Граница ");
q = Convert.ToInt32(Console.ReadLine());
}
public int sum()
{
int s = 0;
for (int i = 0; i < k.Length; i++)
if (k[i] > q) s += k[i];
return s;
} }
Использование созданных классов
class Program
{
static void Main(string[] args)
{
proc1 My = new proc1(); // 1
int s1;
My.output(); // обращение к методу предка
s1=My.sum(); /* обращение к собственному методу,
аналогично можно обращаться и к свойствам предка */
Console.WriteLine("Summa= " + s1);
Console.ReadLine();
} }
При создании экземпляра класса, имеющего предка (строка // 1), запускаются все конструкторы: в первую очередь – конструктор базового класса и затем конструктор класса-наследника. В нашем случае это означает, что будет осуществлен ввод сначала массива и вслед за ним – границы. При наличии большего количества уровней наследования подряд будут запущены конструкторы всех уровней иерархии, начиная с базового.
Если конструкторы не имеют формальных параметров, то при этом никаких проблем не возникает: каждый конструктор независимо от других выполняет свои операторы. Осталось решить вопрос: как обеспечить передачу параметра (ов) конструктору класса-предка, в нашем случае – конструктору 2 ? Проще всего это выполнить с помощью списка инициализации в конструкторе класса-наследника.
public proc1(int k1, int k2): base(k1)
{
q = k2;
}
Запись base(k1)означает, что конструктору базового класса в качестве фактического параметра будет передано значение к1. Пример главной функции в этом случае:
static void Main(string[] args)
{
int s1;
proc1 Myaa = new proc1(6, 20); //обращение к
// конструктору с параметрами
Myaa.output();
s1=Myaa.sum();
Console.WriteLine("Summa= " + s1);
Console.ReadLine();
}
Лучше всего придерживаться следующего правила: при написании конструктора класса-наследника необходимо позаботиться о параметрах непосредственного предка. Таким образом даже при большом количестве уровней иерархии будет обеспечена согласованная работа конструкторов.
3.9. Ссылки на объекты
C# является языком, требующим строгого соблюдения типа при присваивании. Автоматическое преобразование типов, применяемое при работе с обычными переменными, не распространяется на переменные ссылочного типа: ссылочная переменная одного класса не может ссылаться на объект другого класса. Исключение: ссылочной переменой базового класса можно присвоить ссылку на любой класс-наследник. Рассмотрим это на примере.
namespace Virtual1
{
class X
{
public int a;
public X(int i) {a=i;}
}
class Y:X
{
public int b;
public Y(int i, int j ): base(i) {b=j;}
}
class Class1
{
static void Main(string[] args)
{
X x1= new X(10);
X x2;
Y y1=new Y(15,100);
int k;
x2=x1; //допустимо, переменные одного типа
Console.WriteLine("First "+x1.a+" Second "+x2.a);
x2=y1; //допустимо, Y наследник X
Console.WriteLine("First "+x1.a+" Second "+x2.a);
// k=x2.b; ОШИБКА – класс X не имеет переменной b Console.ReadLine(); } } }
Возможность доступа к членам класса зависит от типа ссылочной переменной, а не от типа объекта, на который она ссылается. Поэтому закомментированный оператор будет ошибкой. Наследуемый класс «ничего не знает» о членах класса-наследника!
3.10. Конструктор копирования
В общем случае в C# разрешено присвоение между объектами одного и то же класса. На практике это означает, что мы получим два указателя на один и тот же объект. Вспомните, точно так же было и при присвоении массивов. Для обеспечения создания нового объекта, которому в момент создания были переданы значения данных уже существующего объекта, но при этом под него выделялась собственная область памяти и в дальнейшем эти два объекта были бы полностью независимыми, необходим конструктор копирования. Единственным формальным параметром конструктора копирования всегда является переменная типа «копируемый класс». При наличии конструктора копирования в классе всегда должен быть и обычный конструктор. Обратите внимание на состав формальных параметров конструктора копирования, и Вы поймете, почему это так. Рассмотрим следующий пример.
namespace Construct_Coop
{
class Shape
{
protected double a, h;
public Shape(double x, double y)
{ // обычный конструктор
a = x;
h = y;
}
public Shape(Shape ob)
{ // конструктор копирования
a = ob.a;
h = ob.h;
}
public void NewDan(double x,double y)
{
a = x;
h = y;
}
}
class Tri : Shape
{
protected double area;
public Tri(double x, double y) : base(x, y)
{ // конструктор наследника }
public Tri(Tri ob) :base(ob) // 1
{ // конструктор копирования наследника }
public void Show_area()
{
area = a * h / 2;
Console.WriteLine("S_Treug="+ area);
}
}
class Square : Shape
{
protected double area;
public Square(double x, double y) : base(x, y) { }
public Square(Square ob) : base(ob) { } // 1
public void Show_area()
{
area = a * h;
Console.WriteLine("S_Squar="+ area);
}
}
class Program
{
static void Main(string[] args)
{
Tri my=new Tri(5,12); // работает конструктор
my.Show_area();
Tri w = my; // работает конструктор
w.NewDan(50, 120); // новые данные для w
my.Show_area(); // будут выведены одинаковые значения
w.Show_area();
Tri u = new Tri(w); //работает конструктор копирования
u.NewDan(500, 1200); // новые данные для u
w.Show_area();// будут выведены разные значения
u.Show_area();
Console.ReadLine();
} } }
Обратите внимание на строки // 1: в них имеет место присвоение указателю на базовый класс (Shape) ссылки на класс-наследник (Tri, Square).
3.11. Виртуальные методы
Метод, при определении которого присутствует слово virtual, называется виртуальным. Каждый класс-наследник может иметь собственную версию виртуального метода, называется это переопределением и обозначается словом override. В C# выбор версии виртуального метода осуществляется в соответствии со значением указателя на момент вызова (а не типом указателя, как было в разделе 3.9.). Это делается во время выполнения программы. Указатель во время выполнения программы может указывать на объекты различных классов, поэтому по одному и тому же указателю могут вызываться разные версии виртуального метода. Переопределенные методы обеспечивают поддержку полиморфизма. Полиморфизм позволяет определять в базовом классе методы, которые будут общими для всех наследников, но каждый наследник, в случае необходимости, может иметь их собственные реализации. Естественно, что интерфейсы виртуального метода и всех его версий должны полностью совпадать. Таким образом, применение виртуальных методов позволяет фиксировать интерфейс метода и потом разрабатывать под этот интерфейс новые реализации. Виртуальными могут быть и свойства, и индексаторы.
Рассмотрим это на примере.
namespace Virtual1
{
class Shape
{
protected int a,h;
public Shape (int x,int y)
{
a=x;
h=y;
}
public virtual void Show_area()
{ // вводится виртуальный метод
Console.WriteLine("Площадь будет определена позже");
}
}
class Tri:Shape
{
int s;
public Tri(int x, int y) :base(x, y)
{}
public override void Show_area()
{ //первое переопределение виртуального метода
s=a*h/2;
Console.WriteLine("Площадь треугольника= "+s);
}
}
class Square:Shape
{
int s;
public Square(int x, int y):base(x, y)
{}
public override void Show_area()
{ // второе переопределение виртуального метода
s=a*h;
Console.WriteLine("Площадь четырехугольника= "+s);
}
}
class Class1
{
static void Main(string[] args)
{
Shape q=new Shape(10,30);
q.Show_area();
//
Tri z1=new Tri(5,12);
z1.Show_area();
//
Shape w;
w=z1; // w будет указывать на объект класса Tri
w.Show_area(); // Tri.Show_area()
//
Square w1=new Square(5,12);
w1.Show_area();
//
w=w1; // w будет указывать на объект класса Square
w.Show_area(); //Square.Show_area()
Console.ReadLine();
} } }
Как видно из примера, указатель w имеет тип Shape, но он может указывать на любой наследник Shape. Выбор версии виртуального метода зависит от значения указателя на момент вызова, поэтому вызову w.Show_area(); соответствуют разные версии Show_area().
3.12. Абстрактные методы и классы
В C# существует возможность введения в базовом классе методов, а их реализацию оставить «на потом». Такие методы называют абстрактными. Абстрактный метод автоматически является и виртуальным, но писать это нельзя. Класс, в котором имеется хотя бы один абстрактный метод, тоже называется абстрактным и такой класс может служить только в качестве базового класса. Создать объекты абстрактных классов невозможно, потому что там нет реализации абстрактных методов. Чтобы класс- наследник абстрактного класса не был, в свою очередь, абстрактным (хотя и это не запрещено), там должны содержаться переопределения всех наследованных абстрактных методов.
Модифицируем приведенный выше пример.
namespace Virtual1
{
abstract class Shape
{ /* Создается абстрактный класс,
наличие abstract обязательно! */
public int a,h;
public Shape (int x,int y)
{
a=x;
h=y;
}
public abstract void Show_area();
// реализация не нужна – метод абстрактный,
// фиксируется лишь интерфейс метода
}
class Tri:Shape
{
// см. предыдущий пример,
// наличие public override void Show_area() обязательно!
}
class Square:Shape
{
// см. предыдущий пример
// наличие public override void Show_area() обязательно!
}
class Class1
{
static void Main(string[] args)
{
Shape q; //указатель на абстрактный класс
// q=new Shape(4,6); это ошибка, нельзя создать объект
// абстрактного класса!
q = new Tri(10,20);
q.Show_area();
q = new Square(10,20);
q.Show_area();
Console.ReadLine();
} } }
Указатель на абстрактный класс может указывать на любой класс-наследник.
3.13. Интерфейсы
При практическом программировании иногда возникает необходимость определения действий, которые должны выполняться классом, но без уточнения способов их выполнения. Один способ достижения этого был рассмотрен выше – это абстрактные методы. Абстрактный класс содержит интерфейс метода, но не содержит его реализации. В C# этот подход расширен введением интерфейсов, которые содержат только интерфейсы методов без их реализации. Таким образом, можно полностью отделить интерфейс класса от его реализации. Описание интерфейса:
interface имя_интерфейса
{
Тип_возвращаемого_значения имя _1(параметры);
Тип_возвращаемого_значения имя_2(параметры);
. . . . . . . . .
}
Реализация интерфейса должна находиться в классе-наследнике интерфейса. Ограничение единственного предка в C# на интерфейсы не распространяется: класс может иметь только один класс-предок и сколько угодно интерфейсов-предков. Однако класс должен содержать реализации всех методов, которые содержатся в наследованном интерфейсе. Кроме этого, класс может содержать и собственные методы по общим правилам.
Пример.
namespace Interface1
{
public interface Int1
{ //описание интерфейса из трех методов
double area(double x);
double per();
string type();
}
struct Tri:Int1 //использование интерфейса