Наследование и производные классы (C# vs Java)

Обновлен: Ноябрь 2007

Функциональные возможности существующего класса можно расширить путем создания нового класса, производного от существующего. Производный класс наследует все свойства базового класса, и можно добавлять или переопределять методы и свойства в зависимости от необходимости.

В языке C# как наследование, так и реализация интерфейса определяются оператором :, аналогичным extends и implements в Java. Базовый класс должен всегда занимать крайнее левое положение в объявлении класса.

Как и язык Java, C# не поддерживает множественное наследование. Это значит, что классы не могут наследовать от нескольких классов. Однако для этой цели можно использовать интерфейсы так, как это делается в Java.

В следующем коде определяется класс с именем CoOrds с двумя закрытыми переменными членов x и y, представляя положение точки. Эти переменные вызываются через свойства с именем X и Y соответственно.

public class CoOrds
{
    private int x, y;

    public CoOrds()  // constructor
    {
        x = 0;
        y = 0;
    }

    public int X
    {
        get { return x; }
        set { x = value; }
    }

    public int Y
    {
        get { return y; }
        set { y = value; }
    }
}

Производный от класса CoOrds новый класс с именем ColorCoOrds создается следующим образом.

public class ColorCoOrds : CoOrds

Затем, ColorCoOrds наследует все поля и методы базового класса, к которым можно добавить новые, чтобы получить дополнительные возможности в производном классе, если необходимо. В этом примере добавляется закрытый член и методы доступа, чтобы добавить цвет в класс.

public class ColorCoOrds : CoOrds
{
    private System.Drawing.Color screenColor;


    public ColorCoOrds()  // constructor
    {
        screenColor = System.Drawing.Color.Red;
    }

    public System.Drawing.Color ScreenColor
    {
        get { return screenColor; }
        set { screenColor = value; }
    }
}

Конструктор производного класса неявно вызывает конструктор для базового класса или суперкласса, если использовать терминологию языка Java. При наследовании все конструкторы базового класса вызываются до конструкторов производного класса для того, чтобы классы присутствовали в иерархии классов.

Приведение типов к базовому классу

Как в и языке Java, для доступа к членам и методам производного класса нельзя использовать ссылку на базовый класс, даже если она может содержать допустимую ссылку на объект производного типа.

Ссылаться на производный класс можно при помощи неявной ссылки на производный тип.

ColorCoOrds color1 = new ColorCoOrds();
CoOrds coords1 = color1;

В этом коде ссылка на базовый класс coords1 содержит копию ссылки color1.

Ключевое слово base

Доступ к членам базового класса во вложенном классе можно получить даже если эти базовые члены переопределены в суперклассе при помощи ключевого слова base. Например, можно создать производный класс, содержащий метод с той же подписью, что и в базовом классе. Если перед методом поставить ключевое слово new, то это будет означать, что метод является абсолютно новым, принадлежащим производному классу. С помощью ключевого слова base можно по-прежнему создать метод для доступа к исходному методу в базовом классе.

Предположим, что базовый класс CoOrds имеет метод с именем Invert(), который меняет местами координаты x и y. Этот метод можно подменить в производном классе ColorCoOrds следующим кодом.

public new void Invert()
{
    int temp = X;
    X = Y;
    Y = temp;
    screenColor = System.Drawing.Color.Gray;
}

Как видно, этот метод меняет местами x и y и затем задает серый цвет координат. Доступ к базовой реализации этого метода можно предоставить путем создания другого метода ColorCoOrds, например следующего:

public void BaseInvert()
{
    base.Invert();
}

Затем можно вызвать базовый метод в объекте ColorCoOrds путем вызова метода BaseInvert().

ColorCoOrds color1 = new ColorCoOrds();
color1.BaseInvert();

Следует помнить, что такого же результата можно добиться, назначив ссылку базовому классу на экземпляр ColorCoOrds и вызвав его методы:

CoOrds coords1 = color1;
coords1.Invert();

Выбор конструкторов

Построение объектов базового класса всегда выполняется до любого производного класса. Так, конструктор базового класса выполняется перед конструктором производного класса. Если базовый класс имеет несколько конструкторов, производный класс может выбрать вызываемый конструктор. Например, можно изменить класс CoOrds для добавления второго конструктора, как показано ниже:

public class CoOrds
{
    private int x, y;

    public CoOrds()
    {
        x = 0;
        y = 0;
    }

    public CoOrds(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

Потом можно изменить ColorCoOrds и использовать определенный класс доступных конструкторов при помощи ключевого слова base:

public class ColorCoOrds : CoOrds
{
    public System.Drawing.Color color;

    public ColorCoOrds() : base ()
    {
        color = System.Drawing.Color.Red;
    }

    public ColorCoOrds(int x, int y) : base (x, y)
    {
        color = System.Drawing.Color.Red;
    }
}

В языке Java эта функциональная возможность реализуется, посредством использования ключевого слова super.

Переопределение методов

Производный класс может переопределить метод базового класса, предоставив новую реализацию для объявленного метода. Важным отличием языка Java от C# является то, что по умолчанию методы в Java помечаются как виртуальные, в то время как в языке C# методы должны быть явным образом помечены как виртуальные при помощи модификатора virtual. Переопределение методов доступа к свойствам, а также методов выполняется практически одинаково.

Виртуальные методы

Переопределяемый метод в производном классе объявляется при помощи модификатора virtual. В производном классе для объявления переопределенного метода используется модификатор override.

Модификатор override обозначает метод или свойство производного класса, который заменяет его тем же именем и подписью в базовом классе. Базовый метод, который будет перезаписан, должен быть объявлен как virtual, abstract или override: таким способом нельзя переопределить статический метод или метод, не являющийся виртуальным. Как переопределенный, так и переопределяемый метод или свойство должны иметь одинаковые модификаторы уровня доступа.

В следующем примере показан виртуальный метод с именем StepUp, переопределенный в производном классе модификатором переопределения:

public class CountClass 
{
    public int count; 

    public CountClass(int startValue)  // constructor
    {
        count = startValue;
    }

    public virtual int StepUp()
    {
        return ++count;
    }
}

class Count100Class : CountClass 
{
    public Count100Class(int x) : base(x)  // constructor 
    {
    }

    public override int StepUp()
    {
        return ((base.count) + 100);
    }
}

class TestCounters
{
    static void Main()
    {
        CountClass counter1 = new CountClass(1);
        CountClass counter100 = new Count100Class(1);

        System.Console.WriteLine("Count in base class = {0}", counter1.StepUp());
        System.Console.WriteLine("Count in derived class = {0}", counter100.StepUp());
    } 
}

При выполнении этого кода можно заметить, что конструктор производного класса использует тело метода в базовом классе, позволяя инициализировать член счетчика не дублируя этот код. Ниже приведены выходные данные.

Count in base class = 2

Count in derived class = 101

Абстрактные классы

Абстрактный класс объявляет один или несколько методов или свойств в качестве абстрактных. Такие методы не имеют реализации в классе, объявляющем их, однако абстрактный класс может также содержать неабстрактные методы, то есть методы для которых предоставляется реализация. Экземпляр абстрактного класса не может быть создан непосредственно – только как производный класс. Такие производные классы должны предоставлять реализации для всех абстрактных методов и свойств при помощи ключевого слова override, если только сам производный член не объявлен абстрактным.

В следующем примере объявляется абстрактный класс Employee. Также, можно создать производный класс с именем Manager, предоставляющий реализацию абстрактного метода Show(), определенного в классе Employee.

public abstract class Employee 
{ 
    protected string name;

    public Employee(string name)  // constructor
    { 
        this.name = name;
    }

    public abstract void Show();  // abstract show method
}

public class Manager: Employee
{ 
    public Manager(string name) : base(name) {}  // constructor

    public override void Show()  //override the abstract show method
    {
        System.Console.WriteLine("Name : " + name);
    }
}

class TestEmployeeAndManager
{ 
    static void Main()
    { 
        // Create an instance of Manager and assign it to a Manager reference:
        Manager m1 = new Manager("H. Ackerman");
        m1.Show();

        // Create an instance of Manager and assign it to an Employee reference:
        Employee ee1 = new Manager("M. Knott");
        ee1.Show();  //call the show method of the Manager class
    } 
}

Этот код вызывает реализацию Show(), предоставляемую классом Manager и выводит имена сотрудников на экран. Ниже приведены выходные данные.

Name : H. Ackerman

Name : M. Knott

Интерфейсы

Интерфейс – это своего рода скелет класса, содержащий подписи методов, но не включающий реализации методов. В этом отношении интерфейсы напоминают абстрактные классы, содержащие только абстрактные методы. Интерфейсы C# очень похожи на интерфейсы Java, которые также работают подобным образом.

Все члены интерфейса являются открытыми по определению, и интерфейс не может содержать констант, полей (закрытых элементов данных), конструкторов, деструкторов или какого-либо типа статического члена. Если для членов интерфейса указать любой модификатор, компилятор создаст ошибку.

Для реализации такого интерфейса можно создать производные от интерфейса классы. Такие производные классы должны предоставлять реализации для всех методов интерфейса, если только сам производный класс не объявлен абстрактным.

Объявления класса выполняется аналогичным объявлению в Java образом. В определении интерфейса свойство указывает только его тип, а также определяет, доступно ли он только для чтения, только для записи или для чтения/записи при помощи только ключевых слов get и set. В представленном ниже интерфейсе объявлено одно свойство только для чтения.

public interface ICDPlayer
{ 
    void Play();  // method signature
    void Stop();  // method signature

    int FastForward(float numberOfSeconds);

    int CurrentTrack  // read-only property
    {
        get;
    } 
}

Класс может наследовать от этого интерфейса, если вместо ключевого слова языка Java implements поставить двоеточие. Реализующий класс должен предоставлять определения для всех методов, а также любые обязательные методы доступа к свойствам, как показано ниже:

public class CDPlayer : ICDPlayer
{
    private int currentTrack = 0;

    // implement methods defined in the interface
    public void Play()
    {
        // code to start CD...
    }

    public void Stop()
    {
        // code to stop CD...
    }

    public int FastForward(float numberOfSeconds)
    {
        // code to fast forward CD using numberOfSeconds...

        return 0;  //return success code
    }

    public int CurrentTrack  // read-only property
    { 
        get 
        { 
            return currentTrack; 
        } 
    }

    // Add additional methods if required...
}

Реализация нескольких интерфейсов

Класс может реализовать несколько интерфейсов при помощи следующего синтаксиса.

public class CDAndDVDComboPlayer : ICDPlayer, IDVDPlayer

Если класс реализует несколько интерфейсов, то возможную неоднозначность в именах членов можно разрешить при помощи полного квалификатора имени свойства или метода. Другими словами, производный класс может разрешить конфликт при помощи полного имени метода для указания того, к какому интерфейсу он принадлежит, как в ICDPlayer.Play().

См. также

Основные понятия

Руководство по программированию в C#

Ссылки

Наследование (Руководство по программированию в C#)

Другие ресурсы

Язык программирования C# для разработчиков на Java