Множественное наследование си шарп
Интерфейс представляет некое описание типа, набор компонентов, который должен иметь тип данных. И, собственно, мы не можем создавать объекты интерфейса напрямую с помощью конструктора, как например, в классах:
IMovable m = new IMovable(); // ! Ошибка, так сделать нельзя interface IMovable
В конечном счете интерфейс предназначен для реализации в классах и структурах. Например, реализуем выше определенный интерфейс IMovable:
// применение интерфейса в классе class Person : IMovable < public void Move() < Console.WriteLine("Человек идет"); >> // применение интерфейса в структуре struct Car : IMovable < public void Move() < Console.WriteLine("Машина едет"); >>
При применении интерфейса, как и при наследовании после имени класса или структуры указывается двоеточие и затем идут названия применяемых интерфейсов. При этом класс должен реализовать все методы и свойства применяемых интерфейсов, если эти методы и свойства не имеют реализации по умолчанию.
Если методы и свойства интерфейса не имеют модификатора доступа, то по умолчанию они являются публичными, при реализации этих методов и свойств в классе и структуре к ним можно применять только модификатор public .
Применение интерфейса в программе:
Person person = new Person(); Car car = new Car(); DoAction(person); DoAction(car); void DoAction(IMovable movable) => movable.Move(); interface IMovable < void Move(); >class Person : IMovable < public void Move() =>Console.WriteLine("Человек идет"); > struct Car : IMovable < public void Move() =>Console.WriteLine("Машина едет"); >
В данной программе определен метод DoAction() , который в качестве параметра принимает объект интерфейса IMovable. На момент написания кода мы можем не знать, что это будет за объект — какой-то класс или структура. Единственное, в чем мы можем быть уверены, что этот объект обязательно реализует метод Move и мы можем вызвать этот метод.
Иными словами, интерфейс — это контракт, что какой-то определенный тип обязательно реализует некоторый функционал.
Консольный вывод данной программы:
Реализация интерфейсов по умолчанию
Начиная с версии C# 8.0 интерфейсы поддерживают реализацию методов и свойств по умолчанию. Зачем это нужно? Допустим, у нас есть куча классов, которые реализуют некоторый интерфейс. Если мы добавим в этот интерфейс новый метод, то мы будем обязаны реализовать этот метод во всех классах, применяющих данный интерфейс. Иначе подобные классы просто не будут компилироваться. Теперь вместо реализации метода во всех классах нам достаточно определить его реализацию по умолчанию в интерфейсе. Если класс не реализует метод, будет применяться реализация по умолчанию.
IMovable tom = new Person(); Car tesla = new Car(); tom.Move(); // Walking tesla.Move(); // Driving interface IMovable < void Move() =>Console.WriteLine("Walking"); > class Person : IMovable < >class Car : IMovable < public void Move() =>Console.WriteLine("Driving"); >
В данном случае интерфейс IMovable определяет реализацию по умолчанию для метода Move . Класс Person не реализует этот метод, поэтому он применяет реализацию по умолчанию в отличие от класса Car , который определяет свою реализацию для метода Move.
Стоит отметить, что хотя для объекта класса Person мы можем вызвать метод Move — ведь класс Person применяет интерфейс IMovable , тем не менее мы не можем написать так:
Person tom = new Person(); tom.Move(); // Ошибка - метод Move не определен в классе Person
Множественная реализация интерфейсов
Интерфейсы имеют еще одну важную функцию: в C# не поддерживается множественное наследование, то есть мы можем унаследовать класс только от одного класса, в отличие, скажем, от языка С++, где множественное наследование можно использовать. Интерфейсы позволяют частично обойти это ограничение, поскольку в C# классы и структуры могут реализовать сразу несколько интерфейсов. Все реализуемые интерфейсы указываются через запятую:
class myClass: myInterface1, myInterface2, myInterface3, .
Message hello = new Message("Hello World"); hello.Print(); // Hello World interface IMessage < string Text < get; set; >> interface IPrintable < void Print(); >class Message : IMessage, IPrintable < public string Text < get; set; >public Message(string text) => Text = text; public void Print()=> Console.WriteLine(Text); >
В данном случае определены два интерфейса. Интерфейс IMessage определяет свойство Text, которое представляет текст сообщения. А интерфейс IPrintable определяет метод Print.
Класс Message реализует оба интерфейса и затем применяется в программе.
Интерфейсы в преобразованиях типов
Все сказанное в отношении преобразования типов характерно и для интерфейсов. Поскольку класс Message реализует интерфейс IMessage, то переменная типа IMessage может хранить ссылку на объект типа Message:
// Все объекты Message являются объектами IMessage IMessage hello = new Message("Hello METANIT.COM"); Console.WriteLine(hello.Text); // Hello METANIT.COM // Не все объекты IMessage являются объектами Message, необходимо явное приведение // Message someMessage = hello; // ! Ошибка // Интерфейс IMessage не имеет свойства Print, необходимо явное приведение // hello.Print(); // ! Ошибка // если hello представляет класс Message, выполняем преобразование if (hello is Message someMessage) someMessage.Print();
Преобразование от класса к его интерфейсу, как и преобразование от производного типа к базовому, выполняется автоматически. Так как любой объект Message реализует интерфейс IMessage.
Обратное преобразование — от интерфейса к реализующему его классу будет аналогично преобразованию от базового класса к производному. Так как не каждый объект IMessage является объектом Message (ведь интерфейс IMessage могут реализовать и другие классы), то для подобного преобразования необходима операция приведения типов. И если мы хотим обратиться к методам класса Message, которые не определены в интерфейсе IMessage, но являются частью класса Message, то нам надо явным образом выполнить преобразование типов:
if (hello is Message someMessage) someMessage.Print();
Интерфейсы. Множественное наследование
Множественным наследованием называется ситуация, когда класс объявляет N классов ( N > 1 ) своими непосредственными родителями. В языке C# есть ограничения на множественное наследование . Ситуация здесь такая. Во-первых, у каждого класса родителем, хотя не всегда непосредственным, является класс object . Во-вторых, каждый класс может явно объявить один класс в качестве непосредственного родителя, а также объявить непосредственными родителями один или несколько интерфейсов. Таким образом, в C# допускается множественное наследование интерфейсов и одиночное (не считая наследования от класса object ) наследование классов.
Во многом ограничение множественного наследования классов связано с тем, что оно создает ряд проблем. Они остаются и при множественном наследовании интерфейсов, хотя становятся проще. Рассмотрим две основные проблемы — коллизию имен и наследование от общего предка.
Коллизия имен
Проблема коллизии имен возникает, когда два или более интерфейса имеют методы с одинаковыми именами и сигнатурой. Если сигнатуры разные, то это не приводит к конфликтам. Класс реализует методы обоих интерфейсов, и у него просто появляются перегруженные методы .
Но что следует делать классу наследнику в тех случаях, когда сигнатуры методов совпадают? Возможны две стратегии — склеивание методов и переименование.
Стратегия склеивания применяется тогда, когда класс — наследник интерфейсов — полагает, что методы разных интерфейсов, имеющие одинаковое имя и сигнатуру, задают один и тот же метод, единая реализация которого и должна быть обеспечена наследником. В этом случае наследник строит единственную общедоступную ( public ) реализацию, соответствующую методам всех интерфейсов c единой сигнатурой.
Стратегия переименования исходит из того, что, несмотря на единую сигнатуру, методы разных интерфейсов должны быть реализованы по-разному. В этом случае необходимо переименовать конфликтующие методы. Конечно, переименование можно сделать в самих интерфейсах, но это неправильный путь: наследники не должны требовать изменений своих родителей — они сами должны меняться. Переименование методов интерфейсов иногда невозможно чисто технически, если интерфейсы являются встроенными или поставляются сторонними фирмами. К счастью, мы уже знаем, как производить переименование метода интерфейса в самом классе наследника. Для этого достаточно реализовать в классе методы разных интерфейсов как закрытые, а затем открыть их в классе с переименованием.
Итак, коллизия имен при множественном наследовании интерфейсов хотя и возможна, но разрешима. Разработчик класса может выбрать одну из двух возможных стратегий, наиболее подходящую для данного конкретного случая.
Наследование от общего предка
Проблема наследования от общего предка характерна, в первую очередь, для множественного наследования классов. Если класс C является наследником классов A и B , а те, в свой черед, являются наследниками класса P , то класс наследует свойства и методы своего предка P дважды: один раз получая их от класса A , другой — от B . Это явление называется еще дублирующим наследованием. Для классов ситуация осложняется тем, что классы A и B могли по-разному переопределить методы родителя и для потомков предстоит сложный выбор реализации. Ситуация дублирующего наследования показана на рис. 5.3.
Для интерфейсов сама ситуация дублирующего наследования маловероятна, но возможна, поскольку интерфейс, как и любой класс, может быть наследником другого интерфейса. Поскольку у интерфейсов наследуются только сигнатуры, а не реализации, как в случае классов, проблема дублирующего наследования сводится к проблеме коллизии имен. По-видимому, естественным решением этой проблемы в данной ситуации является склеивание, когда методам, пришедшим разными путями от одного родителя, будет соответствовать единая реализация.
Склеивание и переименование
Приведу пример двух интерфейсов, имеющих методы с одинаковой сигнатурой, и класса — наследника этих интерфейсов, применяющего разные стратегии для конфликтующих методов. У нас уже определен интерфейс IStrings . Предположим, что существует интерфейс ITransform , подобный Istrings :
interface ITransform < ////// Преобразование /// ///результат преобразования string Convert(); ////// Шифрование /// /// код ///результат шифрования string Cipher(string[] code); >
У этих интерфейсов имена и сигнатуры методов совпадают. Вот класс, наследующий оба интерфейса:
////// Наследник двух интерфейсов, /// у методов которых Convert и Cipher /// сигнатуры совпадают. /// Методы Cipher склеиваются, /// Convert - переименовываются /// class TwoInterfaces:IStrings,ITransform < //Опущена часть класса, общая с классом SimpleText //Реализация интерфейсов string IStrings.Convert() < string res = ""; foreach (char sym in text) if (sym != ' ') res += sym.ToString(); res = res.ToLower(); return res; >string ITransform.Convert() < string res = ""; for (int i = text.Length - 1; i >= 0; i--) res += text[i]; return res; > //Переименование закрытых методов public string ConvertOne() < return ((IStrings)this).Convert(); >public string ConvertTwo() < return ((ITransform)this).Convert(); >//Склеивание метода Cipher двух интерфейсов public string Cipher(string[] code) < string s = text; string res = ""; foreach (char sym in s) < int k = code[0].IndexOf(sym); if (k >= 0) res += code[1][k]; else res += sym.ToString(); > return res; > >
Для методов Cipher двух интерфейсов выбрана стратегия склеивания. Для методов Convert выбрана стратегия переименования. Методы интерфейсов реализованы как закрытые методы , а затем в классе объявлены два новых метода с разными именами, являющиеся обертками закрытых методов класса.
Приведу пример работы с объектами класса и интерфейсными объектами:
public void TestTextTwoInterfaces()
Результаты работы показаны на рис. 5.4.