CodeGym /Kursy /C# SELF /Jawna implementacja interfejsu w C#

Jawna implementacja interfejsu w C#

C# SELF
Poziom 23 , Lekcja 4
Dostępny

1. Wprowadzenie

Zazwyczaj, gdy implementujesz interfejs, po prostu tworzysz publiczne metody w swojej klasie, których sygnatury pokrywają się z tymi zadeklarowanymi w interfejsie. To się nazywa niejawną albo publiczną implementacją. Kompilator jest wystarczająco sprytny, żeby zrozumieć: "Aha, ta metoda DoSomething() w klasie MyClass jest do implementacji IDoable.DoSomething()!".

Ale co jeśli:

  • Twoja klasa SmartDevice implementuje interfejs ICamera (który ma metodę TakePicture()) i interfejs IScreen (który też ma metodę TakePicture() — do robienia zrzutu ekranu)?
  • Albo twoja klasa Robot już ma publiczną metodę Reset() do pełnego resetu wszystkich systemów, ale chcesz, żeby implementowała też interfejs IDevice z metodą Reset(), która powinna resetować tylko część ustawień?

W takich przypadkach pojawia się niejednoznaczność albo chęć wyraźnego rozdzielenia funkcjonalności. I tu właśnie pojawia się jawna implementacja interfejsu.

Jawna implementacja interfejsu pozwala ci powiedzieć kompilatorowi: "Ta konkretna metoda jest do implementacji tego właśnie interfejsu i będzie dostępna tylko przez referencję do tego interfejsu". To jak Peter Parker, który strzela pajęczyną tylko wtedy, gdy działa jako Spider-Man, a swoje umiejętności fotografa — gdy działa jako "zwykły" Peter Parker.

2. Kiedy i po co używać jawnej implementacji interfejsu

Gdy implementujesz interfejs w zwykły sposób, jego metody i właściwości stają się częścią publicznego interfejsu twojej klasy. Ale są sytuacje, gdy chcesz, żeby były dostępne tylko przez sam interfejs, a nie bezpośrednio przez klasę. To się zdarza częściej, niż myślisz! Na przykład, jeśli dwa interfejsy wymagają tej samej metody (ale mają inne znaczenie), albo chcesz ograniczyć dostęp do implementacji tylko dla użytkowników, którzy pracują z obiektem wyłącznie przez interfejs.

I tu właśnie przydaje się jawna implementacja interfejsu. To taki sekretny przejście w architekturze programu: z zewnątrz go nie widać, ale wtajemniczeni — przejdą!

Scenariusz 1: Konflikt nazw

Wyobraź sobie, że masz klasę, która implementuje dwa interfejsy, a oba wymagają metody o tej samej nazwie, ale zupełnie innej logice. Na przykład:

interface IWriter
{
    void Print();
}

interface IPrinter
{
    void Print();
}

Chcesz, żeby IWriter.Print() zapisywał tekst do pliku, a IPrinter.Print() wysyłał go na drukarkę. Zwykła implementacja metody o nazwie Print() nie pozwoli ci rozdzielić zachowania. I tu jawna implementacja ratuje sytuację.

Scenariusz 2: Ukrywanie nieaktualnych/technicznych metod interfejsu

Czasem twoja klasa musi zaimplementować metodę interfejsu, ale wcale nie chcesz jej oferować wszystkim użytkownikom klasy (na przykład, członek interfejsu jest potrzebny tylko do wewnętrznej pracy w infrastrukturze).

Scenariusz 3: Ochrona przed przypadkowym wywołaniem

Jeśli metoda interfejsu nie jest przeznaczona do bezpośredniego wywołania (np. wewnętrzny mechanizm frameworka), możesz ją zaimplementować jawnie — wtedy przypadkowy programista nie wywoła jej bezpośrednio przez obiekt klasy.

3. Składnia jawnej implementacji interfejsu

Najważniejsza różnica — przy jawnej implementacji nazywasz metody i właściwości z pełnym kwalifikatorem interfejsu. I żadnych modyfikatorów dostępu (public/private) ani słowa kluczowego override!

Ogólna składnia:


Typ_zwracany NazwaInterfejsu.NazwaMetody(parametry)
{
    // implementacja
}
Składnia jawnej implementacji interfejsu

Wygląda to tak, jakbyś jawnie wskazywał nazwę interfejsu, żeby kompilator i koledzy się nie pogubili, do jakiego kontraktu należy ta implementacja.

Rozwiązywanie konfliktu nazw

Zobaczmy ten przypadek z IWriter i IPrinter. Kontynuujemy nasze przykładowe aplikacje, gdzie mamy klasę raportów:

interface IWriter
{
    void Print();
}

interface IPrinter
{
    void Print();
}

public class Report : IWriter, IPrinter
{
    // Jawna implementacja IWriter.Print
    void IWriter.Print()
    {
        Console.WriteLine("Zapisujemy raport do pliku (Writer)...");
    }

    // Jawna implementacja IPrinter.Print
    void IPrinter.Print()
    {
        Console.WriteLine("Wysyłamy raport na drukarkę (Printer)...");
    }

    // Dodatkowa metoda do wyświetlania użytkownikowi
    public void Show()
    {
        Console.WriteLine("Wyświetlamy raport na ekranie.");
    }
}

Teraz spróbujmy użyć różnych interfejsów:

var report = new Report();

report.Show(); // Zwykła publiczna metoda

// report.Print(); // Błąd! Nie ma metody Print o takiej nazwie w klasie Report

IWriter writer = report;
writer.Print(); // Wywoła implementację IWriter.Print()

IPrinter printer = report;
printer.Print(); // Wywoła implementację IPrinter.Print()

Tutaj metody Print są niedostępne przez sam obiekt report, ale dostępne przez odpowiedni interfejs. To jest sedno jawnej implementacji: tylko przez "port" interfejsowy dostaniesz się do odpowiedniej metody.

Jak to wygląda w pamięci: prosta ilustracja

W zasadzie jawna implementacja "chowa" członka interfejsu w klasie. Dla wizualizacji można sobie wyobrazić taką tabelkę:

Jak się odwołujemy Co się naprawdę wywoła
report.Print()
Błąd kompilacji — nie ma takiej metody
((IWriter)report).Print()
Jawna implementacja
IWriter.Print()
((IPrinter)report).Print()
Jawna implementacja
IPrinter.Print()
report.Show()
Metoda Show klasy

4. Przykład: Interfejs — tylko dla infrastruktury

W prawdziwym projekcie często spotyka się taki scenariusz: klasa musi zaimplementować jakiś techniczny interfejs, ale sama implementacja nie jest potrzebna zwykłym użytkownikom tej klasy.

interface IBroadcastable
{
    void Broadcast();
}

public class SecretMessage : IBroadcastable
{
    void IBroadcastable.Broadcast()
    {
        Console.WriteLine("Tajna wiadomość poszła w eter...");
    }

    public void Reveal()
    {
        Console.WriteLine("Pokazujemy sekret na ekranie.");
    }
}

// W zwykłym kodzie:
var message = new SecretMessage();
message.Reveal();  // Metoda użytkownika

// message.Broadcast(); // Błąd! — nie ma takiej metody

// Tylko infrastruktura wie, co robić:
((IBroadcastable)message).Broadcast();

Tutaj metoda Broadcast() — tylko dla tych, którzy pracują przez kontrakt interfejsu.

5. Jawna implementacja właściwości i indeksatorów

Można jawnie implementować nie tylko metody, ale też właściwości, a nawet indeksatory.

interface IDescribable
{
    string Description { get; }
}

public class Product : IDescribable
{
    // Jawna implementacja właściwości
    string IDescribable.Description => "Opis dostępny tylko przez interfejs";

    // Zwykła publiczna właściwość
    public string Name { get; set; }
}

// Przykład użycia:
var p = new Product { Name = "Gadżet" };
// p.Description; // Błąd! Nie ma takiej właściwości w Product

var descr = ((IDescribable)p).Description;
Console.WriteLine(descr);

6. Przydatne niuanse

Jak działa dziedziczenie z jawną implementacją

Przy dziedziczeniu wszystko działa intuicyjnie: jeśli klasa bazowa implementuje interfejs jawnie, to klasa pochodna dziedziczy to "zachowanie". Jednak jeśli klasa pochodna chce nadpisać implementację metody interfejsu, to się nie uda: jawna implementacja nie może być wirtualna. Czyli nie można nadpisać jawnie zaimplementowanego członka.

Jeśli potrzebujesz takiego zachowania — musisz użyć zwykłej implementacji albo rozważyć wzorzec "metody szablonowej".

Jawna i niejawna implementacja


+----------------+
|   Invoice      |
+----------------+
| Show()         |   // Można wywołać bezpośrednio
+----------------+
| ITxtExportable.Export()   // Tylko przez ITxtExportable
| IJsonExportable.Export()  // Tylko przez IJsonExportable
+----------------+

Specyfika/ograniczenia jawnej implementacji

  • Jawnie zaimplementowane metody i właściwości nie mogą mieć modyfikatorów dostępu. Domyślnie są prywatne dla świata zewnętrznego i dostępne tylko przez interfejs.
  • Takich członków nie można zrobić static, virtual, abstract ani override.
  • Nie można się odwołać do jawnie zaimplementowanego członka przez obiekt klasy bezpośrednio (tylko przez zmienną typu interfejsu).
  • Jeśli interfejs dziedziczy po innym interfejsie, można jawnie zaimplementować członków z dowolnego poziomu hierarchii.

7. Zalety jawnej implementacji

Jawna implementacja to nie tylko trik składniowy do rozwiązywania kolizji. Ma kilka ważnych powodów istnienia:

Rozwiązywanie konfliktów nazw (The Big One): To główny i najbardziej oczywisty powód. Jeśli dwa interfejsy, które implementujesz, deklarują metody (lub właściwości) o tych samych sygnaturach, jawna implementacja pozwala ci dać osobne, specyficzne dla każdego interfejsu implementacje. Bez tego miałbyś niejednoznaczność.
Przykład z życia: Wyobraź sobie, że masz drukarkę. Może być jednocześnie skanerem. Interfejs IPrinter ma metodę Print(), a interfejs IScanner — metodę Scan(). Ale co, jeśli oba interfejsy miałyby metodę ProcessDocument()? Jawna implementacja pozwala ci zrobić IPrinter.ProcessDocument() do drukowania i IScanner.ProcessDocument() do skanowania, i będą działać inaczej.

Ukrywanie szczegółów implementacji i czystość API klasy: Metody zaimplementowane jawnie nie są częścią publicznego API twojej klasy. Są dostępne tylko po rzutowaniu obiektu na typ interfejsu. To bardzo przydatne, gdy chcesz, żeby pewna funkcjonalność była dostępna tylko "po kontrakcie", a nie jako część ogólnego zachowania twojego obiektu.
Przykład: Tworzysz skomplikowany instrument finansowy, np. CreditCard. Może implementować IPayable (do płatności) i IAdminConfigurable (do wewnętrznych ustawień, np. limitów). Metoda IAdminConfigurable.SetLimit() nie powinna być dostępna dla każdego, kto po prostu ma CreditCard. Powinna być dostępna tylko dla systemu administracyjnego, który pracuje z CreditCard jako z IAdminConfigurable. Jawna implementacja pozwala trzymać SetLimit() ukrytą przed ogólnym dostępem do CreditCard, czyniąc API klasy czystszym i bezpieczniejszym.

Gwarancja kontraktu: Czasem metoda w twojej klasie przypadkowo ma tę samą sygnaturę, co metoda w interfejsie, ale nie chcesz, żeby była jego implementacją. Na przykład masz klasę MyList z metodą Clear() do czyszczenia swojego stanu. Jeśli zdecydujesz, że MyList powinien implementować IList<T>, który też ma Clear(), to domyślnie twój MyList.Clear() stanie się implementacją IList<T>.Clear(). Jeśli ich logika powinna być inna, jawna implementacja IList<T>.Clear() pozwala ci je rozdzielić.

Niejawna (Implicit) vs. Jawna (Explicit) implementacja

Żeby zobaczyć różnicę, spójrzmy na krótką tabelkę porównawczą:

Charakterystyka Niejawna (Implicit) implementacja Jawna (Explicit) implementacja
Dostępność Dostępna zarówno przez klasę, jak i przez interfejs. Dostępna tylko przez interfejs.
Modyfikator dostępu Zazwyczaj public (albo protected itd.). Brak modyfikatora dostępu (nie może być public).
Składnia
public ReturnType MethodName(Params) { ... }
ReturnType InterfaceName.MethodName(Params) { ... }
Rozwiązywanie kolizji Nie rozwiązuje, metoda będzie dla wszystkich interfejsów z tą sygnaturą. Rozwiązuje kolizje, pozwalając na różne implementacje.
Czystość API klasy Metoda jest częścią publicznego API klasy. Metoda nie jest częścią publicznego API klasy.
Przeznaczenie Dla ogólnych, jednoznacznych implementacji interfejsów. Dla rozwiązywania kolizji lub ukrywania specyficznej logiki.

Jak widzisz, to dwie strony tej samej monety i każda ma swoje zastosowanie. W większości przypadków będziesz używać niejawnej implementacji, bo jest prostsza i wygodniejsza. Jawna implementacja to narzędzie do specyficznych, ale ważnych zadań.

8. Typowe błędy przy jawnej implementacji interfejsów

Błąd nr 1: metoda niewidoczna przez obiekt klasy.
Jawnie zaimplementowane metody są niedostępne przez instancję klasy bezpośrednio. Często pojawia się zamieszanie: kompilator mówi, że nie ma metody, choć jest — tylko "ukryta". Rozwiązanie: rzutuj obiekt na typ interfejsu, np. (IMyInterface)obj.Method().

Błąd nr 2: nieprawidłowe użycie modyfikatorów dostępu.
Przy jawnej implementacji nie można podawać modyfikatorów typu public czy override. Próba zrobienia tego skończy się błędem kompilacji — kompilator tego nie przyjmie.

Błąd nr 3: próba nadpisania jawnie zaimplementowanej metody w klasie pochodnej.
Jeśli metoda interfejsu jest jawnie zaimplementowana w klasie bazowej, nie można jej nadpisać w dziedziczącej. To ograniczenie trzeba brać pod uwagę przy projektowaniu hierarchii klas z interfejsami.

1
Ankieta/quiz
Pojęcie interfejsu, poziom 23, lekcja 4
Niedostępny
Pojęcie interfejsu
Interfejsy: podstawy i kontrakty
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION