Skip to main content

C# vs Java. Интерфейсы в C#

· 10 min read
Andrey Ganyushkin

Интерфейс - контракт (набор методов и констант), описывающий какие есть способы взаимодействовать с объектом, который реализует этот контракт. С другой стороны, тот кто заявляет о поддержке какого-либо контракта обязан реализовать все методы описанные в контракте.

Java

C# vs Java. Интерфейсы в Java

C#

Реализовать интерфейс в C# может класс или структура.

Интерфейс может предоставлять определения (реализацию в случае default interface methods) для:

  • Methods
  • Properties
  • Indexers
  • Events

Создание интерфейса

interface IPlant {
string? RuName { get; } // property
string LatName { get; } // property
string? EnName { get; } // property

string GetName(); // method
}

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

NOTE: Растение может иметь несколько названий на разных языках и структура названия растения сложнее чем строка. Метод GetName должен выбрать название в соответствии с приоритетом и представить его в виде строки.

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

class Tree : IPlant
{
public string LatName { get; init; }
public string? RuName { get; init; }
public string? EnName { get; init; }

public string GetName()
{
if (RuName is not null && RuName.Length > 0)
{
return RuName;
}
else if (EnName is not null && EnName.Length > 0)
{
return EnName;
}
else if (LatName is not null && LatName.Length > 0)
{
return LatName;
}
throw new InvalidOperationException("The object must be initialized before use.");
}
}

var Plant = new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
};

var PlantName = Plant.GetName();

explicit / явная реализация

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

При реализации метода мы должны указать к какому интерфейсу относится метод и не указывать модификатор доступа.

Модификатор доступа не имеет смысла так как реализуемый метод не доступен через объект типа класса, а только через тип интерфейса.

class Tree : IPlant
{
public string? RuName { get; init; }
public string LatName { get; init; }
public string? EnName { get; init; }

string IPlant.GetName()
{
if (RuName is not null && RuName.Length > 0)
{
return RuName;
}
else if (EnName is not null && EnName.Length > 0)
{
return EnName;
}
else if (LatName is not null && LatName.Length > 0)
{
return LatName;
}
throw new InvalidOperationException("The object must be initialized before use.");
}
}

Вызвать метод мы можем толко приведя тип к IPlant

var PlantName = (Plant as IPlant).GetName();

// или так
IPlant PlantInterface = new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
};
var PlantName = PlantInterface.GetName();

// или так
IPlant PlantInterface = Plant;
var PlantName = PlantInterface.GetName();

Явная реализация делает недоступным метод для типа класса. Чтобы решить эту проблемы мы можем реализовать метод кнутри класса, который обратится к explicit реализации вот так

class Tree : IPlant
{
...

public string GetName() => (this as IPlant).GetName();
}

При множественном наследовании такой подход позволяет выбрать какой метод какого интерфейса предоставляет данные для GetName метода класса.

множественное наследование

Имплементировать несколько интерфейсов в C# классе мы можем. Бороться с конфликтами множественного наследования нам помогает явная реализация интерфейса.

Можно реализовать метод с одинаковым именем для разных нитерфейсов и вызывать их можно будет приведя объект к типу интерфейса.

interface IPlant
{
string? RuName { get; }
string LatName { get; }
string? EnName { get; }

string GetName();
}

interface IVisualizationEntry
{
string GetName();
}

class Tree : IPlant, IVisualizationEntry
{
public string? RuName { get; init; }
public string LatName { get; init; }
public string? EnName { get; init; }

string IPlant.GetName()
{
if (RuName is not null && RuName.Length > 0)
{
return RuName;
}
else if (EnName is not null && EnName.Length > 0)
{
return EnName;
}
else if (LatName is not null && LatName.Length > 0)
{
return LatName;
}
throw new InvalidOperationException("The object must be initialized before use.");
}

string IVisualizationEntry.GetName()
{
if (LatName is not null && LatName.Length > 0)
{
return LatName;
}
throw new InvalidOperationException("The object must be initialized before use.");
}

public string GetName() => (this as IPlant).GetName();
}

В разных ситуация мы можем приводить этот объект к нужному типу и вызывать требуемый метод

// чтобы получить имя растения
var PlantName = (Plant as IPlant).GetName();

// или так, чтобы получить название для визуализации
var VisualizationName = (Plant as IVisualizationEntry).GetName();

Также нам доступен метод GetName класса, который вернет результат реализации IPlant.GetName

var PlantName = Plant.GetName();

Модификаторы доступа

Доступ по умолчанию для членов интерфейса — public. Можно использовать другие без специфических для интерфейсов ограничений.

Interface members are public by default because the purpose of an interface is to enable other types to access a class or struct. Interface member declarations may include any access modifier.

Interfaces declared directly within a namespace can be public or internal and, just like classes and structs, interfaces default to internal access.

Access Modifiers (C# Programming Guide)

Дженерики

создадим обобщенную коллекцию

interface IPlantCollection<T>
{
bool Add(T plant);
}

реализуем коллекцию для IPlant

class PlantCollection : IPlantCollection<IPlant>
{
// ...
public bool Add(IPlant plant)
{
// ...
return true;
}
}

теперь просто используем это

var Collection = new PlantCollection();

Collection.Add(
new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
}
);

Про дженерики отдельно можно посмотреть здесь.

Расширение интерфейса другим интерфейсом

Интерфейс может быть расширен другим интерфейсом.

Множественное наследование разрешено.

interface ICoreEntry
{
bool IsAvailableForSearch();
}

interface IPlant : ICoreEntry
{
string? RuName { get; }
string LatName { get; }
string? EnName { get; }

string GetName();
}

теперь нужно реализовать два метода

class Tree : IPlant
{
...

public bool IsAvailableForSearch() => true;

public string GetName()
{
...
}
}

Статические члены в интерфейсе

статические методы

interface IPlant
{
string? RuName { get; }
string LatName { get; }
string? EnName { get; }

string GetName();

static string FormatName(IPlant plant) => $"PLANT: {plant.GetName()}";
}

Не наследуется и доступен только по имени типа интерфейса в котором объявлен

var FormattedName = IPlant.FormatName(Plant); // OK

var FormattedName = Plant.FormatName(Plant); // ERROR!

var FormattedName = Tree.FormatName(Plant); // ERROR!; class Tree : IPlant, IVisualizationEntry

var FormattedName = (Plant as IPlant).FormatName(Plant); // ERROR!

статические поля (константы)

Можно определить константу в интерфейсе

interface IPlant
{
static readonly int SORING_WEIGHT = 1;
string GetName();
}

Такая константа не наследуется и доступна только по имени типа интерфейса в котором объявлена

var Weight = IPlant.SORING_WEIGHT; // OK

var Weight = Plant.SORING_WEIGHT; // ERROR!

var Weight = Tree.SORING_WEIGHT; // ERROR!; class Tree : IPlant, IVisualizationEntry

var Weight = (Plant as IPlant).SORING_WEIGHT; // ERROR!

статические члены для потомков

Через описанные выше подходы нельзя определить в интерфейсе static метод который должен быть реализован в классе реализующем интерфейс. Для того чтобы иметь возможность накладывать ограничения или требования на статические методы в реализующем классе мы можем использовать static abstract.

static abstract - можно объявить для всех типов элементов, кроме полей.

Например нам нужно чтобы в классе присетствовал статический метод для проверки все ли поля были инициализированы. Сделать это можно так:

в интервейса определяем сигнатуру метода наличие которого будем требовать от реализации

interface IPlant
{
string GetName();

static abstract bool IsInitialized(IPlant plant);
}

далее, реализуем интерфейс. И нам потребуется реализовать статический метод как того требует интерфейс

class Tree : IPlant
{
...

public static bool IsInitialized(IPlant plant)
{
return plant.GetName() is not null;
}
}

так как IsInitialized реализован в классе Tree то использовать это му можем следующим образом

var InitializedFlag = Tree.IsInitialized(Plant);

Методы по умолчанию

Методы по умолчанию - это методы с реализацией сразу в интерфейсе.

Ничего особенно в реализации нет. Просто метод с реализацией

interface IPlant
{
string? RuName { get; }
string LatName { get; }
string? EnName { get; }

string GetName();

bool IsSortable() => true;
}

NOTE: чтобы использовать метод IsSortable нужно привести объект к типу интерфейса IPlant. Например

class Tree : IPlant { ... }
...

var Plant = new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
};

var flag = Plant.IsSortable(); // ERROR, method IsSortable is not accessable

var flag = (Plant as IPlant).IsSortable(); // OK, works

переопределение методов по умолчанию

Довольно странным способом мы можем переопределить метод по умолчанию.

interface ICoreEntry
{
bool IsAvailableForSearch();

bool IsSortable() => false;
}

interface IPlant : ICoreEntry
{
string? RuName { get; }
string LatName { get; }
string? EnName { get; }

string GetName();

new bool IsSortable() => true;
}

var flag = (Plant as ICoreEntry).IsSortable(); // вернет false
var flag = (Plant as IPlant).IsSortable(); // вернет true

без new работать будет но IDE будет подсказывать что мы не ямно закрыли метод из базового интерфейса.

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

interface ICoreEntry
interface ICoreEntry
{
bool IsAvailableForSearch();

bool IsSortable() { return false; }
}

interface IPlant : ICoreEntry
{
string? RuName { get; }
string LatName { get; }
string? EnName { get; }

string GetName();

bool ICoreEntry.IsSortable() { return true; }
}

var flag = (Plant as ICoreEntry).IsSortable(); // вернет true
var flag = (Plant as IPlant).IsSortable(); // вернет true

Приватные методы в интерфейсе

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

Interface member declarations may include any access modifier.

Q: Зачем это нужно? A: Чтобы скрыть детали реализации.

Преобразование типов

cast object to interface

при присвоении тип будет преобразован к типу интерфейса

var Plant = new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
};

IPlant Entry1 = Plant; // works
ICoreEntry Entry2 = Plant; // works

// or
void ToDoSomethingWithPlant(IPlant plant)
{
...
}

ToDoSomethingWithPlant(Plant);

cast interface to object

var TreeEntry = new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
};

IPlant PlantObject = TreeEntry; // works, cast Tree -> IPlant

Tree TreeEntryAgain1 = PlantObject; // ERROR

Tree TreeEntryAgain2 = (Tree) PlantObject; // works

// хороший способ избежать ошибок
if (PlantObject is Tree TreeObject)
{
// to do something wtih `TreeObject`
}

cast List<Tree\> to List<IPlant\>

var TreePool = new List<Tree>()
{
new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
},
new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
}
};

List<IPlant> PlantPool = TreePool; // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs0029?f1url=%3FappId%3Droslyn%26k%3Dk(CS0029)

Мы не можем напрямую преобразовать список с объектами к списку List<IPlant>. Поэтому нужно сделать что-то подобное:

List<IPlant> PlantPool1 = (List<IPlant>)(from tree in TreePool select tree as IPlant);

List<IPlant> PlantPool2 = (from tree in TreePool select tree as IPlant).ToList();

List<IPlant> PlantPool3 = TreePool.Select(c => (IPlant)c).ToList();

List<IPlant> PlantPool4 = TreePool.ToList<IPlant>();

List<IPlant> PlantPool5 = TreePool.Cast<IPlant>().ToList();

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

Это возможно. Но лучше так не делать.

struct - в первую очередь это value-type. Реализация интерфейса приведет к потерям производительности boxing/unboxing при использовании интерфейса.

некоторые детали в обсуждении

Но если очень хочется:

interface IPlant
{
string? RuName { get; }
string LatName { get; }
string? EnName { get; }
}

struct Tree : IPlant
{
public string? RuName { get; init; }
public string LatName { get; init; }
public string? EnName { get; init; }
}

var TreeEntry = new Tree
{
RuName = "Берёза повислая",
LatName = "Betula pendula",
EnName = null
};