Интерфейс - контракт (набор методов и констант), описывающий какие есть способы взаимодействовать с объектом, который реализует этот контракт. С другой стороны, тот кто заявляет о поддержке какого-либо контракта обязан реализовать все методы описанные в контракте.
Реализовать интерфейс в C# может класс или структура.
Интерфейс может предоставлять определения (реализацию в случае default interface methods) для:
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();
Данный подход позволяет реализовать метод интерфейса в классе но доступен этот метод будет только у объекта с типом интерфейса. Т.е. при реализации мы можем предоставить реализацию функции, которая будет доступна при манипуляциях с объектами преобразованными к типу интерфейса.
При реализации метода мы должны указать к какому интерфейсу относится метод и не указывать модификатор доступа.
Модификатор доступа не имеет смысла так как реализуемый метод не доступен через объект типа класса, а только через тип интерфейса.
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: Чтобы скрыть детали реализации.
при присвоении тип будет преобразован к типу интерфейса
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);
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`
}
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 - в первую очередь это 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
};