Interfejsy #
Interfejsy są podobne do klas abstrakcyjnych, ale z kluczową różnicą: definiują zachowania, a nie mogą definiować stanu (pól). Analogiczną strukturą z C++ byłyby klasy ze wszystkimi składowymi będącymi czysto wirtualnymi metodami.
- Składowymi interfejsu mogą być metody, właściwości, zdarzenia i indeksery. Od C# 8 mogą również zawierać składowe statyczne (w tym stałe) oraz domyślne implementacje metod.
- Interfejsy nie mogą zawierać pól.
- Składowe są niejawnie publiczne i abstrakcyjne.
- Klasy i struktury wspierają implementowanie wielu interfesjów.
Definicja interfejsu wygląda następująco, tutaj przykład interfejsu IEnumerator
z przestrzeni nazw System.Collections
:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
Zwyczajowo, nazwy interfejsów poprzedzamy literą I
. Implementowanie interfejsu polega na podaniu implementacji wszystkich jego składowych, które nie mają domyślnej implementacji.
public class Counter : IEnumerator
{
public int Count { get; private set; }
public Counter(int count) => Count = count;
public bool MoveNext() => Count-- > 0;
public object Current => Count;
public void Reset()
{
throw new NotSupportedException();
}
}
Referencje są polimorficzne ze wszystkimi interfejsami, które implementuje dany typ.
IEnumerator e = new Counter(10);
while (e.MoveNext())
Console.Write(e.Current);
Console.WriteLine();
Rozszerzanie interfejsów #
Interfejsy mogą rozszerzać inne interfejsy, dziedzicząc wszystkie ich składowe.
public interface IUndoable
{
void Undo();
}
public interface IRedoable : IUndoable
{
void Redo();
}
Jawna implementacja składowych #
Składowe interfejsów możemy jawnie implementować, podając z którego interfejsu ta składowa pochodzi. Jest to przydatne, gdy nazwy składowych różnych interfejsów ze sobą kolidują.
public interface IFoo1 { void Bar(); }
public interface IFoo2 { void Bar(); }
public class Foo : IFoo1, IFoo2
{
public void Bar()
{
Console.WriteLine("Implementation of IFoo1.Bar");
}
void IFoo2.Bar()
{
Console.WriteLine("Implementation of IFoo2.Bar");
}
}
Do jawnie zaimplementowanych składowych możemy odwołać się tylko po rzutowaniu typu do tego konkretnego interfejsu.
Foo foo = new Foo();
foo.Bar(); // Implementation of IFoo1.Bar
((IFoo1)foo).Bar(); // Implementation of IFoo1.Bar
((IFoo2)foo).Bar(); // Implementation of IFoo2.Bar
Pakowanie (Boxing) #
Wywoływanie składowych z interfejsu na strukturze nie powoduje operacji pakowania. Natomiast rzutowanie struktury do interfejsu powoduje jej spakowanie (boxing), czyli skopiowanie na stertę.
public interface IShape
{
float Area();
}
public struct Rectangle : IShape
{
public float Width { get; set; }
public float Height { get; set; }
public Rectangle(float width, float height)
{
Width = width;
Height = height;
}
public float Area() => Width * Height;
}
Rectangle rectangle = new Rectangle(4.0f, 5.0f);
float area = rectangle.Area(); // no boxing
IShape shape = rectangle; // boxing
Domyślne implementacje (C# 8) #
Od C# 8 interfejsy mogą dostarczać domyślne implementacje składowych. Wtedy w implementujących typach dostarczenie własnej implementacji staje się opcjonalne.
public interface ILogger
{
void Message(string message)
{
Console.WriteLine(message);
}
}
public class Logger : ILogger
{
public void Message(string message) // optional
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss}: {message}");
}
}
Jeżeli typ nie dostarcza własnej implementacji, domyślną implementację można wywołać jedynie przez referencję typu interfejsu.
Statyczne składowe interfejsów (C# 8) #
Jako, że składowe statyczne nie są częścią stanu instancji, interfejsy mogą je definiować.
public interface ILogger
{
public const string DefaultFile = "default.log";
public static string LogPrefix { get; set; } = "Log: ";
void Message(string message)
{
Console.WriteLine($"{LogPrefix} {message}");
}
}
Dziedziczenie statyczne (C# 11) #
Interfejsy umożliwiają dziedziczenie składowych statycznych - normalnie, tak jak w C++, składowe statyczne nie są dziedziczone. Składowe w interfejsach możemy oznaczać słówkami virtual
i abstract
, będą one wtedy dziedziczone na zasadach identycznych co dla dziedziczenia klas.
public interface IDescriptable
{
static abstract string Description { get; }
static virtual string Category => "General";
}
public class Cat : IDescriptable
{
public static string Description => "It's a cat";
public static string Category => "Mammal"; // optional
}
public class Book : IDescriptable
{
public static string Description => "It's a book";
}