Для кого эта статья?

Данная статья отлично подойдет тем, кто только начинает изучать интерфейсы, а также тем, кто их изучил, но не до конца понимает, для чего их использовать и зачем они нужны. Это нормально, так как когда я изучал интерфейсы, первым вопросом был: "А зачем?" :)

Содержание

  1. Что такое интерфейсы?

  2. Зачем нужны интерфейсы?

  3. Интерфейсные ссылки

  4. Ключевые слова as и is

  5. Наследования интерфейсов

  6. Явная реализация интерфейса

  7. Почему не абстрактный класс?

Что такое интерфейсы?

Интерфейсы представляют собой ссылочный тип, который может содержать в себе набор свойств и методов без реализации (однако начиная с версии C# 8.0 есть возможность реализовать метод в интерфейсе, но об этом чуть позже). И в дальнейшем какой-либо класс может реализовать данный интерфейс. Звучит страшно? Давайте разбираться.

Давайте рассмотрим интерфейсы на практике. Интерфейсы объявляются с помощью ключевого слова interface. Далее вы пишите название интерфейса. Как правило, к названию интерфейса следует приписывать вначале большую букву I, что означает Interface. Называть его следует так, чтобы название описывало поведение интерфейса, то есть что он делает и вообще для чего он. Например, если мы хотим объявить интерфейс, который будет содержать метод Say, то неплохим названием будет ISpeakable. Сейчас все покажу.

Давайте для начала создадим данный интерфейс

interface ISpeakable
{
 void Say();
}

Как видите, реализацию мы не написали. Теперь давайте создадим класс Person, который будет реализовывать данный интерфейс.

public class Person : ISpeakable
{
		public void Say()
		{
    		Console.WriteLine("Hi!");
		}
}

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

Теперь мы можем создать объект данного класса и вызвать функцию Say. В результате у нас на консоль выведется сообщение Hi!

Person p = new Person();
p.Say();

Начиная с версии C# 8.0 мы можем указывать реализацию методов прямо в интерфейсе. И тогда, когда мы будем реализовывать интерфейс, нам не обязательно нужно будет реализовывать этот метод. Это что-то типо реализации по умолчанию.

Зачем нужны интерфейсы?

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

Проблема

Допустим у нас есть 2 класса, которые содержат поле Name и метод Move.

public class Person
{
		public string Name { get; set; }

		public void Move() 
    {
    		Console.WriteLine("Person is moving...");
    }
}
public class Animal
{
		public string Name { get; set; }

		public void Move()
		{
				Console.WriteLine("Animal is moving...");
		}
}

У них отличается реализация метода Move: в одном движется человек, в другом - животное.

И, допустим, нам нужно создать функцию, которая будет отвечать за движение человека или животного до какой-то точки. Проще говоря, в этой функции человек или животное будут куда-то ходить. В параметры мы хотим передавать наш объект и он будет куда-то идти. Например:

void PersonGoToPoint(Person p)
{
		Console.WriteLine($"{p.Name} go to point");
}

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

void AnimalGoToPoint(Animal animal)
{
		Console.WriteLine($"{animal.Name} go to point");
}

А если данные классы увеличатся? Если у нас появится, к примеру, еще и класс Fish? Ведь она тоже движется (хоть и плавает) и может дойти доплыть до какой-то точки. Нам придется создать еще один метод. Это вообще не классно и код у нас будет раздуваться так же, как и живот от кока-колы.

Давайте попробуем сделать иначе. Давайте возьмем и создадим интерфейс IMovable, который будет в себе хранить свойство Name и метод Move

public interface IMoveable
{
		string Name { get; set; }
		void Move();
}

И теперь пускай наши 2 класса будут реализовывать данный интерфейс

public class Person : IMovable
{
		public string Name { get; set; }

		public void Move() 
		{
        Console.WriteLine("Person is moving...");
		}
}
public class Animal : IMovable
{
		public string Name { get; set; }

    public void Move()
    {
            Console.WriteLine("Animal is moving...");
    }
}

Вроде бы, что изменилось? А теперь смотрите, что можно сделать. Мы можем в параметры функции, которая отвечает за движение до какой-то точки передать не конкретный класс, а наш интерфейс.

void GoToPoint(IMoveable moveable)
{
		Console.WriteLine($"{moveable.Name} go to point...");
}

То есть мы передали туда наш интерфейс. Что это значит? Теперь при вызове функции мы можем передать туда любой класс, который реализует данный интерфейс. Можно провести аналогию с наследованием: если в параметрах функции указан родительский класс, то мы можем туда передать все его дочерние классы.

IMoveable p = new Person();
p.Name = "John";
p.Move();
GoToPoint(p);

Мы можем создавать на основе интерфейса объект Person и передать его в функцию. Так как объект Person реализует данный интерфейс, мы спокойно можем это сделать. Однако, если бы класс Person не реализовал данный интерфейс, мы бы такого сделать не смогли. Кстати, нам не обязательно создавать класс от имени интерфейса. Мы можем просто создать объект класса Person и передать в функцию.

Person p = new Person();
p.Name = "John";
p.Move();
GoToPoint(p);

В результате мы получаем огромную гибкость. Смотрите, при добавлении класса Fish нам не нужно будет создавать отдельный метод под него. Достаточно будет классу Fish реализовать интерфейс IMovable. Затем мы можем в параметры функции передать объект класса Fish.

Интерфейсные ссылки

Самое интересное, что мы только что сделали интерфейсную ссылку :)

Интерфейсная ссылка - это переменная, которая ссылается на любой объект, который реализует данный интерфейс. Когда мы написали следующее:

IMoveable p = new Person();
p.Name = "John";
p.Move();
GoToPoint(p);

, то мы создали интерфейсную ссылку IMovable p. То есть наш объект p может ссылаться на любой объект, который будет реализовывать данный интерфейс.

Ключевые слова as и is

В C# возможно с помощью ключевого слова as определить, поддерживает ли данный тип тот или иной интерфейс. Например, мы создали объект класса Person

Person p = new Person();

И после этого мы хотим создать интерфейсную ссылку IMovable и приравнять к объекту p

IMovable imovable = p;

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

IMovable imovable = p as IMovable;

После as мы указываем интерфейс, который должен реализовывать объект p. Дальше мы можем провести проверку на null и если все хорошо, то можем проводить какие-то над ним действия

if (p != null) 
{
 		// Все ок, можем что-то делать 
}
else 
{
 		// данный объект равен null, значит он не реализует данный интерфейс
}

Проверить, был ли реализован нужный интерфейс, можно также с помощью ключевого слова is. Если запрашиваемый объект не совместим с указанным интерфейсом, возвращается значение false, а если совместим, то можно спокойно выполнять какие-либо действия.

if (p is IMovable) 
{
		// Все ок, можем что-то делать 
}
else 
{
		// данный объект не реализует данный интерфейс
}

Наследование интерфейсов

Один интерфейс может наследовать другой. Например: есть пользователь и админ. К примеру, пользователь может только читать данные, а админ еще и писать что-то. У нас будет 2 интерфейса.

interface IUser
{
  	void Read();
}

interface IAdmin : IUser
{
		void Write();
}

Так как и пользователь, и админ могут читать данные, то метод Read должен быть у обоих. Поэтому интерфейс IAdmin наследует интерфейс IUser, тем самым наследуя метод Read().

public class User : IUser
{
		public void Read()
    {
    		Console.WriteLine("User is reading...")
    }
}

public class Admin : IAdmin
{
		public void Read()
    {
    		Console.WriteLine("Admin is reading...");
    }
    
    public void Write()
    {
    		Console.WriteLine("Admin is writing...");
    }
}

При реализации интерфейса IUser мы реализуем только метод Read, а при реализации интерфейса IAdmin, мы реализуем методы Read и Write.

Явная реализация интерфейса

Допустим, у нас такая ситуация: есть 2 интерфейса. У этих интерфейсов одинаковое название методов. И в одном классе мы реализуем эти 2 интерфейса. Но как компилятор поймет, какой метод из какого интерфейса мы реализуем? Ведь названия одинаковые. Для этого мы можем явно реализовать интерфейс.

Допустим, что у нас есть 2 интерфейса IMovable и IMovable2

interface IMovable
{
		void Move();
}

interface IMovable2
{
		void Move();
}

И, допустим, у нас есть класс Person, который реализует эти 2 метода. Чтобы явно указать из какого метода реализуется метод Move, мы должны перед названием метода написать название интерфейса, от которого идет метод:

[Название_интерфейса].[Название_метода]

public class Person : IMovable, IMovable2
{
		public string Name { get; set; }

		void IMovable.Move()
		{
				Console.WriteLine("Move from iMovable1");
		}

		void IMovable2.Move()
		{
				Console.WriteLine("Move from IMovable2");
		}
}

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

Person p = new Person();
((IMovable)p).Move();
((IMovable2)p).Move();

Фактически здесь мы объект p преобразуем в тип данных IMovable (а это мы можем сделать, так как объект p реализует данный интерфейс) и вызываем метод Move уже не от Person, а от IMovable. Вот таким образом мы можем явно указывать реализацию интерфейса

Почему не абстрактный класс?

Давайте разберемся, чем отличается абстрактный класс от интерфейса.

Абстрактный класс — это класс, у которого не реализован один или больше методов. Интерфейс — это абстрактный класс, у которого ни один метод не реализован, все они публичные и нет переменных класса. Правда, начиная с C# 8.0 в интерфейсах методы могут быть реализованы, а также не обязательно, чтобы методы были публичные. Давайте тогда по пунктам посмотрим отличия:

  • У интерфейса не может быть полей

  • Класс может реализовать несколько интерфейсов. В случае абстрактного класса, класс может наследовать только один абстрактный класс, так как в С# нет множественного наследования

Можно сказать, что интерфейс является абстрактным классом без полей. Но нельзя сказать, что абстрактный класс является интерфейсом.

Заключение

Сегодня мы разобрали основы работы с интерфейсами, что они из себя представляют и для чего они нужны? Разумеется, это далеко не все, про интерфейсы очень много написано, но в рамках данной статьи я затронул основы, чтобы помочь вам разобраться в интерфейсах и продолжать свой путь к становлению самым лучшим разработчиком. Я в вас верю и у вас все получится. Спасибо за внимание и всем успехов!

Доброго времени суток. Сегодня хочу поговорить об интерфейсах в рамках языка C#. Обсудим, что они из себя представляют и для чего они нужны.