Udostępnij za pośrednictwem


domyślne metody interfejsu

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Problem z championem: https://github.com/dotnet/csharplang/issues/52

Streszczenie

Dodano obsługę metod rozszerzenia wirtualnego — metody w interfejsach z konkretnymi implementacjami. Klasa lub struktura, która implementuje taki interfejs, musi mieć jedną najbardziej specyficzną implementację dla metody interfejsu, zaimplementowaną przez klasę lub strukturę albo dziedziczona z klas bazowych lub interfejsów. Metody rozszerzenia wirtualnego umożliwiają autorowi interfejsu API dodawanie metod do interfejsu w przyszłych wersjach bez przerywania zgodności źródła lub danych binarnych z istniejącymi implementacjami tego interfejsu.

Są one podobne do języka Java "Metody domyślne".

(Na podstawie prawdopodobnej metody implementacji) ta funkcja wymaga odpowiedniej obsługi w interfejsie wiersza polecenia (CLI)/CLR. Programy korzystające z tej funkcji nie mogą być uruchamiane we wcześniejszych wersjach platformy.

Motywacja

Główne motywacje dla tej funkcji to

Szczegółowy projekt

Składnia interfejsu zostaje rozszerzona, aby umożliwić

  • deklaracje składowe, które deklarują stałe, operatory, konstruktory statyczne i typy zagnieżdżone;
  • ciało metody, indeksatora, właściwości lub akcesora zdarzenia (czyli domyślna implementacja);
  • deklaracje składowe, które deklarują pola statyczne, metody, właściwości, indeksatory i zdarzenia;
  • deklaracje członków przy użyciu składni jawnej implementacji interfejsu; i
  • Modyfikatory dostępu jawnego (domyślny dostęp to public).

Członkowie z ciałami umożliwiają interfejsowi dostarczenie "domyślnej" implementacji metody w klasach i strukturach, które nie dostarczają własnej implementacji.

Interfejsy nie powinny zawierać stanu wystąpienia. Pola statyczne są teraz dozwolone, ale pola wystąpień nie są dozwolone w interfejsach. Automatyczne właściwości instancji nie są obsługiwane w interfejsach, ponieważ niejawnie deklarowałyby ukryte pole.

Metody statyczne i prywatne umożliwiają refaktoryzację i organizację kodu używanego do implementowania publicznego interfejsu API.

Nadpisywanie metody w interfejsie musi korzystać z jawnej składni implementacji interfejsu.

Błędem jest deklarowanie typu klasy, typu struktury lub typu wyliczenia w zakresie parametru typu, który został zadeklarowany z użyciem variance_annotation. Na przykład deklaracja C poniżej jest błędem.

interface IOuter<out T>
{
    class C { } // error: class declaration within the scope of variant type parameter 'T'
}

Konkretne metody w interfejsach

Najprostszą formą tej funkcji jest możliwość deklarowania metody w interfejsie, który jest metodą z treścią.

interface IA
{
    void M() { WriteLine("IA.M"); }
}

Klasa, która implementuje ten interfejs, nie musi implementować określonej metody.

class C : IA { } // OK

IA i = new C();
i.M(); // prints "IA.M"

Ostatnim zastąpieniem IA.M w C klasy jest metoda M zadeklarowana w IA. Należy pamiętać, że klasa nie dziedziczy elementów członkowskich ze swoich interfejsów; ta funkcja nie została zmieniona:

new C().M(); // error: class 'C' does not contain a member 'M'

W obrębie członka instancji interfejsu this ma typ interfejsu zewnętrznego.

Modyfikatory w interfejsach

Składnia interfejsu jest złagodzona, aby umożliwić stosowanie modyfikatorów do jego elementów. Dozwolone są następujące elementy: private, protected, internal, public, virtual, abstract, sealed, static, externi partial.

Element członkowski interfejsu, którego deklaracja zawiera treść, jest elementem członkowskim virtual, chyba że jest używany modyfikator sealed lub private. Modyfikator virtual może być używany dla członka funkcji, który inaczej byłby niejawnie virtual. Podobnie, mimo że abstract jest wartością domyślną dla elementów członkowskich interfejsu bez treści, modyfikator może być jawnie podany. Nie-wirtualny członek może zostać zadeklarowany przy użyciu słowa kluczowego sealed.

Jest błędem, jeśli członek funkcji private lub sealed interfejsu nie ma ciała. Element członkowski funkcji private może nie mieć modyfikatora sealed.

Modyfikatory dostępu mogą być używane w przypadku elementów interfejsu dla wszystkich dozwolonych typów. Poziom dostępu public jest domyślny, ale może zostać jawnie podany.

otwarty problem: Musimy określić dokładne znaczenie modyfikatorów dostępu, takich jak protected i internal, oraz które deklaracje zastępują lub nie zastępują je (w interfejsie pochodnym) lub implementują je (w klasie implementującej interfejs).

Interfejsy mogą deklarować static członków, w tym zagnieżdżone typy, metody, indeksatory, właściwości, zdarzenia i konstruktory statyczne. Domyślny poziom dostępu dla wszystkich elementów członkowskich interfejsu to public.

Interfejsy mogą nie deklarować konstruktorów wystąpień, destruktorów ani pól.

Zamknięty problem: Czy deklaracje operatorów powinny być dozwolone w interfejsie? Prawdopodobnie nie operatory konwersji, ale co z innymi? decision: Operatorzy mogą z wyjątkiem dla operatorów konwersji, równości i nierówności.

Zamknięty Problem: Czy new powinny być dozwolone w deklaracjach członków interfejsu, ukrywających członków bazowych interfejsów? Decyzja: Tak.

Zamknięty problem: Obecnie nie zezwalamy na partial na interfejsie lub jego członkach. Wymagałoby to oddzielnej propozycji. Decyzja: Tak. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface

Jawna implementacja w interfejsach

Jawne implementacje pozwalają programistom na zapewnienie najbardziej konkretnej implementacji wirtualnego elementu członkowskiego w interfejsie, w którym kompilator lub środowisko uruchomieniowe nie odnalazłoby inaczej takiej implementacji. Deklaracja implementacji może jawnie zaimplementować określoną metodę interfejsu podstawowego, kwalifikując deklarację z nazwą interfejsu (w tym przypadku nie jest dozwolony modyfikator dostępu). Implementacje niejawne nie są dozwolone.

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    void IA.M() { WriteLine("IB.M"); } // Explicit implementation
}
interface IC : IA
{
    void M() { WriteLine("IC.M"); } // Creates a new M, unrelated to `IA.M`. Warning
}

Jawne implementacje w interfejsach nie mogą być deklarowane sealed.

Elementy członkowskie funkcji typu public virtual w interfejsie mogą być implementowane jawnie tylko w interfejsie pochodnym (poprzez kwalifikowanie nazwy w deklaracji za pomocą typu interfejsu, który pierwotnie zadeklarował metodę, oraz jawne pominięcie modyfikatora dostępu). Element członkowski musi być dostępny tam, gdzie jest implementowany.

Ponowne abstrakcjonowanie

Wirtualna (konkretna) metoda zadeklarowana w interfejsie może zostać ponownie uznana za abstrakcyjną w interfejsie pochodnym.

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    abstract void IA.M();
}
class C : IB { } // error: class 'C' does not implement 'IA.M'.

Modyfikator abstract jest wymagany w deklaracji IB.M, aby wskazać, że IA.M jest ponownie abstrahowany.

Jest to przydatne w interfejsach pochodnych, w których domyślna implementacja metody jest nieodpowiednia, a bardziej odpowiednia implementacja powinna być dostarczana przez implementację klas.

Najbardziej specyficzna reguła implementacji

Wymagamy, aby każdy interfejs i każda klasa miały najbardziej specyficzną implementację dla każdego wirtualnego członka z implementacji, które pojawiają się w typie lub jego bezpośrednich i pośrednich interfejsach. najbardziej specyficzna implementacja to unikalna implementacja, która jest bardziej specyficzna niż każda inna implementacja. Jeśli nie ma implementacji, sam członek jest uważany za najbardziej konkretną implementację.

Jedna implementacja M1 jest uważana za bardziej specyficzną niż inna implementacja M2, jeśli M1 jest zadeklarowana na typie T1, M2 jest zadeklarowana na typie T2, a w którymkolwiek z przypadków

  1. T1 zawiera T2 między interfejsami bezpośrednimi lub pośrednimi albo
  2. T2 jest typem interfejsu, ale T1 nie jest typem interfejsu.

Na przykład:

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
    void IA.M() { WriteLine("IC.M"); }
}
interface ID : IB, IC { } // compiles, but error when a class implements 'ID'
abstract class C : IB, IC { } // error: no most specific implementation for 'IA.M'
abstract class D : IA, IB, IC // ok
{
    public abstract void M();
}
public class E : ID { } // Error. No most specific implementation for 'IA.M'

Najbardziej specyficzna reguła implementacji gwarantuje, że konflikt (tj. niejednoznaczność wynikająca z dziedziczenia diamentowego) jest jawnie rozwiązany przez programistę w momencie wystąpienia konfliktu.

Ponieważ obsługujemy jawne ponowne abstrakcje w interfejsach, możemy to również wprowadzić w klasach.

abstract class E : IA, IB, IC // ok
{
    abstract void IA.M();
}

Zamknięty problem: czy powinniśmy obsługiwać jawne implementacje abstrakcyjne interfejsu w klasach? decyzja : BRAK

Ponadto jest to błąd, jeśli w deklaracji klasy najbardziej specyficzna implementacja niektórych metod interfejsu jest abstrakcyjną implementacją zadeklarowaną w interfejsie. Jest to istniejąca reguła, która jest ponownie używana przy użyciu nowej terminologii.

interface IF
{
    void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'

Istnieje możliwość, że właściwość wirtualna zadeklarowana w interfejsie ma najbardziej specyficzną implementację dla jej get akcesorium w jednym interfejsie i najbardziej specyficzną implementację dla jej set akcesorium w innym interfejsie. Jest to uważane za naruszenie najbardziej konkretnej reguły implementacji i generuje błąd kompilatora.

metody static i private

Ponieważ interfejsy mogą teraz zawierać kod wykonywalny, warto wyodrębnić wspólny kod z metodami prywatnymi i statycznymi. Teraz zezwalamy na te elementy w interfejsach.

Zamknięty problem: Czy powinniśmy obsługiwać metody prywatne? Czy powinniśmy obsługiwać metody statyczne? decyzja : TAK

otwarcie problemu: czy powinniśmy zezwolić, aby metody interfejsu miały protected lub internal lub inne poziomy dostępu? Jeśli tak, jakie są semantyki? Czy są one domyślnie virtual? Jeśli tak, czy istnieje sposób, aby nie były one wirtualne?

Zamknięty problem: jeśli obsługujemy metody statyczne, czy powinniśmy obsługiwać (statyczne) operatory? decyzja : TAK

Wywołania interfejsu podstawowego

Składnia w tej sekcji nie została zaimplementowana. Pozostaje to aktywna propozycja.

Kod w typie pochodzącym z interfejsu z domyślną metodą może jawnie wywołać implementację "podstawową" interfejsu.

interface I0
{
   void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
   override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
   override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
   // an explicit override that invoke's a base interface's default method
   void I0.M() { I2.base.M(); }
}

Metoda instancji (niestatyczna) może wywołać implementację dostępnej metody instancji w bezpośrednim interfejsie bazowym z pominięciem wirtualizacji, nazywając ją przy użyciu składni base(Type).M. Jest to przydatne, gdy konieczne zastąpienie wynikające z dziedziczenia diamentowego jest rozwiązywane przez delegowanie do jednej konkretnej implementacji podstawowej.

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
    override void IA.M() { WriteLine("IC.M"); }
}

class D : IA, IB, IC
{
    void IA.M() { base(IB).M(); }
}

Kiedy uzyskuje się dostęp do elementu członkowskiego virtual lub abstract przy użyciu składni base(Type).M, wymagane jest, aby Type zawierało unikatowe najbardziej specyficzne przesłonięcie dla M.

Łączenie klauzul bazowych

Interfejsy zawierają teraz typy. Te typy mogą być używane w klauzuli podstawowej jako interfejsy podstawowe. Podczas wiązania klauzuli podstawowej może być konieczna znajomość zestawu interfejsów podstawowych w celu powiązania tych typów (np. do wyszukiwania w nich i rozpoznawania dostępu chronionego). Znaczenie klauzuli podstawowej interfejsu jest zatem definiowane cyklicznie. Aby przerwać cykl, dodamy nowe reguły języka odpowiadające podobnej regule już obowiązującej dla klas.

Podczas określania znaczenia interface_base interfejsu podstawowe interfejsy są tymczasowo zakładane jako puste. Intuicyjnie zapewnia to, że znaczenie klauzuli podstawowej nie może rekursywnie zależeć od siebie.

Użyliśmy następujących reguł:

"Gdy klasa B pochodzi z klasy A, jest to błąd czasu kompilacji dla A, aby zależeć od B. klasy zależy bezpośrednio od jej bezpośredniej klasy bazowej (jeśli istnieje) i bezpośrednio zależy od klasy , w której jest natychmiast zagnieżdżona (jeśli istnieje). Biorąc pod uwagę tę definicję, kompletny zestaw klas , od których zależy jakaś klasa, jest refleksyjnym i przechodnim zamknięciem relacji polegającej na bezpośredniej zależności .

Popełnieniem błędu kompilacji jest sytuacja, gdy interfejs dziedziczy bezpośrednio lub pośrednio z samego siebie. Interfejsy podstawowe interfejsu to jawne interfejsy podstawowe i ich podstawowe interfejsy. Innymi słowy, zestaw interfejsów bazowych jest pełnym przechodnim zamknięciem jawnych interfejsów bazowych, ich własnych jawnych interfejsów bazowych i tak dalej.

Dostosowujemy je w następujący sposób:

Gdy klasa B dziedziczy z klasy A, jest błędem czasu kompilacji, jeśli klasa A zależy od klasy B. Klasa bezpośrednio zależy od jej bezpośredniej klasy bazowej (jeśli istnieje), a bezpośrednio zależy od typu , w którym jest bezpośrednio zagnieżdżona (jeśli istnieje).

Gdy interfejs IB rozszerza interfejs IA, występuje błąd kompilacji, jeśli IA zależy od IB. Interfejs bezpośrednio zależy od jego bezpośrednich interfejsów podstawowych (jeśli istnieje) i bezpośrednio zależy od typu, w którym jest natychmiast zagnieżdżony (jeśli istnieje).

Biorąc pod uwagę te definicje, kompletny zestaw typów , od których typ zależy, jest odruchowe i przechodnie zamknięcie bezpośrednio zależy od relacji.

Wpływ na istniejące programy

Przedstawione tutaj zasady nie mają wpływu na znaczenie istniejących programów.

Przykład 1:

interface IA
{
    void M();
}
class C: IA // Error: IA.M has no concrete most specific override in C
{
    public static void M() { } // method unrelated to 'IA.M' because static
}

Przykład 2:

interface IA
{
    void M();
}
class Base: IA
{
    void IA.M() { }
}
class Derived: Base, IA // OK, all interface members have a concrete most specific override
{
    private void M() { } // method unrelated to 'IA.M' because private
}

Te same reguły dają podobne wyniki do analogicznej sytuacji obejmującej domyślne metody interfejsu:

interface IA
{
    void M() { }
}
class Derived: IA // OK, all interface members have a concrete most specific override
{
    private void M() { } // method unrelated to 'IA.M' because private
}

Zamknięty problem: upewnij się, że jest to zamierzony wynik specyfikacji. decyzja : TAK

Rozpoznawanie metody środowiska uruchomieniowego

zamknięty problem: Specyfikacja powinna opisywać algorytm rozpoznawania metod środowiska uruchomieniowego w obliczu domyślnych metod interfejsu. Musimy upewnić się, że semantyka jest spójna z semantykami języka, np. które zadeklarowane metody nie zastępują ani nie implementują metody internal.

API wsparcia CLR

Aby kompilatory wykrywały, kiedy kompilują środowisko uruchomieniowe obsługujące tę funkcję, biblioteki dla takich środowisk uruchomieniowych są modyfikowane w celu anonsowania tego faktu za pośrednictwem interfejsu API omówionego w https://github.com/dotnet/corefx/issues/17116. Dodajemy

namespace System.Runtime.CompilerServices
{
    public static class RuntimeFeature
    {
        // Presence of the field indicates runtime support
        public const string DefaultInterfaceImplementation = nameof(DefaultInterfaceImplementation);
    }
}

Otwórz problem: Czy jest to najlepsza nazwa funkcji CLR? Funkcja CLR ma znacznie więcej niż tylko to (np. rozluźnia ograniczenia ochrony, obsługuje przesłonięcia w interfejsach itp.). Być może powinien być nazywany jak "konkretne metody w interfejsach", czy "cechy"?

Dalsze obszary do określenia

  • [ ] Warto katalogować rodzaje efektów zgodności źródłowej i binarnej spowodowane dodaniem domyślnych metod interfejsu i przesłonięć do istniejących interfejsów.

Wady

Wniosek ten wymaga skoordynowanej aktualizacji specyfikacji CLR (w celu obsługi konkretnych metod w interfejsach i rozwiązywaniu metod). Dlatego jest to dość kosztowne i może warto to zrobić w połączeniu z innymi funkcjami, które według naszych przewidywań również będą wymagały zmian CLR.

Alternatywy

Żaden.

Nierozwiązane pytania

  • Otwarte pytania są wywoływane w ramach powyższej propozycji.
  • Zobacz również https://github.com/dotnet/csharplang/issues/406, aby uzyskać listę otwartych pytań.
  • Szczegółowa specyfikacja musi opisywać mechanizm rozwiązywania używany w czasie wykonywania, aby wybrać dokładną metodę do wywołania.
  • Interakcje metadanych utworzonych przez nowe kompilatory i używane przez starsze kompilatory muszą być szczegółowo wypracowane. Na przykład musimy upewnić się, że używana reprezentacja metadanych nie powoduje dodania domyślnej implementacji w interfejsie, aby przerwać istniejącą klasę, która implementuje ten interfejs podczas kompilowania przez starszy kompilator. Może to mieć wpływ na reprezentację metadanych, której możemy użyć.
  • Projekt musi uwzględniać współdziałanie z innymi językami i istniejącymi kompilatorami dla innych języków.

Rozwiązane pytania

Przesłonięcie abstrakcyjne

Wcześniejsza specyfikacja robocza zawierała możliwość "odtworzenia" dziedziczonej metody:

interface IA
{
    void M();
}
interface IB : IA
{
    override void M() { }
}
interface IC : IB
{
    override void M(); // make it abstract again
}

Moje notatki dla 2017-03-20 pokazały, że postanowiliśmy tego nie pozwolić. Istnieją jednak co najmniej dwa przypadki użycia:

  1. Interfejsy API języka Java, z którymi niektórzy użytkownicy tej funkcji mają nadzieję na współdziałanie, zależą od tej funkcji.
  2. Programowanie z wykorzystaniem cech przynosi korzyści. Reabstraction jest jednym z elementów funkcji języka 'traity' (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Następujące czynności są dozwolone w ramach zajęć:
public abstract class Base
{
    public abstract void M();
}
public abstract class A : Base
{
    public override void M() { }
}
public abstract class B : A
{
    public override abstract void M(); // reabstract Base.M
}

Niestety nie można refaktoryzować tego kodu jako zestawu interfejsów (cech), chyba że jest to dozwolone. Zgodnie z zasadą Jared chciwości, powinno być dozwolone.

Zamknięty problem: Czy ponowne abstrakcjonowanie powinno być dozwolone? [TAK] Moje notatki były błędne. Uwagi LDM mówią, że ponowne abstrakcje są dozwolone w interfejsie. Nie w klasie.

Modyfikator wirtualny a modyfikator zapieczętowany

Z Aleksey Tsingauz:

Postanowiliśmy zezwolić na modyfikatory jawnie określone na członkach interfejsu, chyba że istnieje powód, aby zabronić niektórych z nich. Powoduje to interesujące pytanie dotyczące modyfikatora wirtualnego. Czy powinno być wymagane w przypadku elementów członkowskich z domyślną implementacją?

Możemy powiedzieć, że:

  • Jeśli nie określono implementacji ani wirtualnej, ani zapieczętowanej, zakładamy, że element członkowski jest abstrakcyjny.
  • Jeśli istnieje implementacja i nie określono ani abstrakcyjny, ani zapieczętowany, zakładamy, że element członkowski jest wirtualny.
  • Modyfikator sealed jest wymagany, aby metoda nie była ani wirtualna, ani abstrakcyjna.

Alternatywnie można powiedzieć, że modyfikator wirtualny jest wymagany dla wirtualnego członka. Tj. jeśli element członkowski z implementacją nie jest jawnie oznaczony modyfikatorem wirtualnym, nie jest ani wirtualny, ani abstrakcyjny. Takie podejście może zapewnić lepsze doświadczenie, kiedy metoda zostaje przeniesiona z klasy do interfejsu.

  • metoda abstrakcyjna pozostaje abstrakcyjna.
  • metoda wirtualna pozostaje wirtualna.
  • metoda bez żadnego modyfikatora nie pozostaje ani wirtualna, ani abstrakcyjna.
  • nie można zastosować modyfikatora zapieczętowanego do metody, która nie jest przesłoniętą.

Co myślisz?

problem zamknięty: Czy konkretna metoda (z implementacją) powinna być niejawnie virtual? [TAK]

Decyzje: podjęte w LDM 2017-04-05:

  1. niewirtualne powinny być jawnie wyrażone za pośrednictwem sealed lub private.
  2. sealed to słowo kluczowe używane do tworzenia członków instancji interfejsu, które mają niewirtualne ciała.
  3. Chcemy zezwolić na wszystkie modyfikatory w interfejsach
  4. Domyślna dostępność dla elementów członkowskich interfejsu jest publiczna, w tym dla typów zagnieżdżonych
  5. prywatne elementy członkowskie funkcji w interfejsach są niejawnie zapieczętowane, a sealed nie jest na nich dozwolone.
  6. Klasy prywatne (w interfejsach) są dozwolone i mogą być zamknięte, co oznacza zamknięcie w sensie, w jakim klasy są zamykane.
  7. Bez dobrej propozycji częściowe zmiany są wciąż niedozwolone w interfejsach ani ich członkach.

Zgodność binarna 1

Gdy biblioteka udostępnia domyślną implementację

interface I1
{
    void M() { Impl1 }
}
interface I2 : I1
{
}
class C : I2
{
}

Rozumiemy, że implementacja I1.M w C jest I1.M. Co zrobić, jeśli zestaw zawierający I2 zostanie zmieniony w następujący sposób i ponownie skompilowany

interface I2 : I1
{
    override void M() { Impl2 }
}

ale C nie jest ponownie skompilowany. Co się stanie po uruchomieniu programu? Wywołanie (C as I1).M()

  1. Uruchamia I1.M
  2. Uruchamia I2.M
  3. Zgłasza jakiś błąd środowiska uruchomieniowego

decyzja : Podjęto 2017-04-11: Uruchamia I2.M, która jest jednoznacznie najbardziej specyficznym przesłonięciem w czasie wykonywania.

Akcesory zdarzeń (zamknięte)

Zamknięte zgłoszenie: Czy zdarzenie może zostać częściowo zastąpione?

Rozważmy ten przypadek:

public interface I1
{
    event T e1;
}
public interface I2 : I1
{
    override event T
    {
        add { }
        // error: "remove" accessor missing
    }
}

Ta "częściowa" implementacja zdarzenia jest zabroniona, ponieważ, podobnie jak w klasie, składnia deklaracji zdarzenia nie zezwala na użycie tylko jednego akcesora; należy podać zarówno oba, jak i żadnego. Można uzyskać ten sam efekt, zezwalając, aby w definicji dostępnik abstrakcyjny remove był niejawnie abstrakcyjny przez brak ciała:

public interface I1
{
    event T e1;
}
public interface I2 : I1
{
    override event T
    {
        add { }
        remove; // implicitly abstract
    }
}

Należy pamiętać, że jest to nowa (proponowana) składnia. W bieżącej gramatyki akcesory zdarzeń mają obowiązkową treść.

Zamknięte zgłoszenie: Czy akcesor zdarzenia może być (niejawnie) abstrakcyjny przez pominięcie treści, podobnie jak metody w interfejsach i akcesorach właściwości są (niejawnie) abstrakcyjne przez pominięcie treści?

decyzja: (2017-04-18) Nie, deklaracje zdarzeń wymagają obu konkretnych akcesorów (lub żadnego).

Reabstrakcja w klasie (zamknięte)

Zamknięty problem: Powinniśmy potwierdzić, że jest to dozwolone (w przeciwnym razie dodanie domyślnej implementacji będzie zmianą powodującą niezgodność):

interface I1
{
    void M() { }
}
abstract class C : I1
{
    public abstract void M(); // implement I1.M with an abstract method in C
}

Decyzja: (2017-04-18) Tak, dodanie treści do deklaracji składowej interfejsu nie powinno przerywać C.

Zapieczętowane nadpisanie (zamknięte)

Poprzednie pytanie niejawnie zakłada, że modyfikator sealed można zastosować do override w interfejsie. Jest to sprzeczne ze specyfikacją roboczą. Czy chcemy zezwolić na uszczelnienie przesłonięcia? Należy wziąć pod uwagę efekty zgodności źródłowej i binarnej wynikające z zapewniania ochrony.

zamknięty problem: Czy należy zezwolić na uszczelnienie przesłonięcia?

Decyzja: (2017-04-18) Nie zezwalajmy na sealed przesłonięcia w interfejsach. Jedynym zastosowaniem sealed w elementach członkowskich interfejsu jest uczynienie ich niewirtualnymi w ich początkowej deklaracji.

Dziedziczenie i klasy diamentów (zamknięte)

Szkic propozycji preferuje przesłonięcia klas nad przesłonięciami interfejsów w scenariuszach dziedziczenia diamentowego.

Wymagamy, aby każdy interfejs i klasa miały najbardziej szczegółowe zastąpienie dla każdej metody interfejsu spośród przesłonięć obecnych w typie lub jego bezpośrednich i pośrednich interfejsach. najbardziej szczegółowe przesłonięcie jest unikatowym przesłonięciem, które jest bardziej szczegółowe niż jakiekolwiek inne. Jeśli nie ma przesłonięcia, sama metoda jest uważana za najbardziej konkretne przesłonięcie.

Jedno zastąpienie M1 jest uznawane za bardziej szczegółowe niż inne zastąpienie M2, jeśli M1 jest zadeklarowane w typie T1, M2 jest zadeklarowane w typie T2, i albo

  1. T1 zawiera T2 między interfejsami bezpośrednimi lub pośrednimi albo
  2. T2 jest typem interfejsu, ale T1 nie jest typem interfejsu.

Scenariusz jest taki

interface IA
{
    void M();
}
interface IB : IA
{
    override void M() { WriteLine("IB"); }
}
class Base : IA
{
    void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
    static void Main()
    {
        IA a = new Derived();
        a.M();           // what does it do?
    }
}

Należy potwierdzić to zachowanie (lub zdecydować w inny sposób)

Zamknięta kwestia: Potwierdzenie powyższej specyfikacji wersji roboczej dla najbardziej specyficznego nadpisania w kontekście mieszanych klas i interfejsów (priorytet ma klasa nad interfejsem). Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#diamonds-with-classes.

Metody interfejsu a struktury (zamknięte)

Istnieją pewne niefortunne interakcje między domyślnymi metodami interfejsu i strukturami.

interface IA
{
    public void M() { }
}
struct S : IA
{
}

Należy pamiętać, że elementy członkowskie interfejsu nie są dziedziczone:

var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'

W związku z tym klient musi opakować strukturę, aby wywołać metody interfejsu

IA s = default(S); // an S, boxed
s.M(); // ok

Boks w ten sposób niweczy główne korzyści typu struct. Ponadto wszelkie metody mutacji nie będą miały wyraźnego wpływu, ponieważ działają na kopii w pudełku struktury:

interface IB
{
    public void Increment() { P += 1; }
    public int P { get; set; }
}
struct T : IB
{
    public int P { get; set; } // auto-property
}

T t = default(T);
Console.WriteLine(t.P); // prints 0
(t as IB).Increment();
Console.WriteLine(t.P); // prints 0

Zamknięty problem: Co możemy zrobić w tym celu:

  1. Zabraniaj struct dziedziczenia implementacji domyślnej. Wszystkie metody interfejsu będą traktowane jako abstrakcyjne w struct. Następnie możemy poświęcić trochę czasu później, aby zdecydować, jak to usprawnić.
  2. Wymyślij jakąś strategię generowania kodu, która pozwala uniknąć boksowania. Wewnątrz metody, takiej jak IB.Increment, typ this może być podobny do parametru typu ograniczonego do IB. W związku z tym, aby uniknąć ograniczania możliwości wywołującego, metody nieabstrakcyjne będą dziedziczone z interfejsów. Może to znacznie zwiększyć zakres pracy kompilatora i implementacji CLR.
  3. Nie martw się o to i po prostu zostaw to jako brodawkę.
  4. Inne pomysły?

Decyzja: Nie martwić się o to i po prostu zostawić go jako brodawkę. Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#structs-and-default-implementations.

Wywołania interfejsu podstawowego (zamknięte)

Ta decyzja nie została wdrożona w języku C# 8. Składnia base(Interface).M() nie jest zaimplementowana.

Specyfikacja robocza sugeruje składnię wywołań interfejsu podstawowego inspirowanego językiem Java: Interface.base.M(). Musimy wybrać składnię, przynajmniej dla początkowego prototypu. Mój ulubiony element to base<Interface>.M().

Zamknięty problem: Jaka jest składnia wywołania bazowego członka?

Decyzja: Składnia jest base(Interface).M(). Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation. Interfejs tak nazwany musi być interfejsem podstawowym, ale nie musi być bezpośrednim interfejsem podstawowym.

otwarty problem: Czy wywołania interfejsu podstawowego powinny być dozwolone w składowych klas?

Decyzja: Tak. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation

Zastępowanie elementów członkowskich interfejsu niepublikowego (zamknięte)

W interfejsie elementy członkowskie inne niż publiczne z interfejsów podstawowych są zastępowane za pomocą modyfikatora override. Jeśli jest to "jawne" zastąpienie, które nazywa interfejs zawierający element członkowski, modyfikator dostępu zostanie pominięty.

Zamknięta kwestia: Jeśli jest to "niejawne" zastąpienie, które nie wskazuje interfejsu, czy modyfikator dostępu musi być zgodny?

decyzja: Tylko członkowie publiczni mogą być niejawnie zastępowani, a dostęp musi być zgodny. Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.

otwarta kwestia: Czy modyfikator dostępu jest wymagany, opcjonalny czy pominięty w przypadku jawnego przesłonięcia, takiego jak override void IB.M() {}?

otwarty problem: czy override wymagane, opcjonalne lub pominięte w jawnym przesłonięciu, takim jak void IB.M() {}?

Jak zaimplementować element członkowski interfejsu niepublicznego w klasie? Być może trzeba to zrobić jawnie?

interface IA
{
    internal void MI();
    protected void MP();
}
class C : IA
{
    // are these implementations?  Decision: NO
    internal void MI() {}
    protected void MP() {}
}

Zamknięty problem: Jak zaimplementować członka interfejsu niepublikowego w klasie?

Decyzja: Można jawnie zaimplementować tylko elementy członkowskie interfejsu niepublikowego. Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.

decyzja: słowo kluczowe override nie jest dozwolone dla członków interfejsu. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member

Zgodność binarna 2 (zamknięta)

Rozważ następujący kod, w którym każdy typ znajduje się w osobnym zestawie

interface I1
{
    void M() { Impl1 }
}
interface I2 : I1
{
    override void M() { Impl2 }
}
interface I3 : I1
{
}
class C : I2, I3
{
}

Rozumiemy, że implementacja I1.M w C jest I2.M. Co zrobić, jeśli zestaw zawierający I3 zostanie zmieniony w następujący sposób i ponownie skompilowany

interface I3 : I1
{
    override void M() { Impl3 }
}

ale C nie jest ponownie skompilowany. Co się stanie po uruchomieniu programu? Wywołanie (C as I1).M()

  1. Uruchamia I1.M
  2. Uruchamia I2.M
  3. Uruchamia I3.M
  4. 2 lub 3, deterministyczne
  5. Zgłasza jakiś wyjątek środowiska uruchomieniowego

decyzja: Zgłaszanie wyjątku (5). Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#issues-in-default-interface-methods.

Czy zezwolić na partial w interfejsie? (zamknięte)

Biorąc pod uwagę, że interfejsy mogą być używane w sposób analogiczny do sposobu użycia klas abstrakcyjnych, może być przydatne zadeklarowanie ich partial. Byłoby to szczególnie przydatne w obliczu generatorów.

Propozycja: Usuń ograniczenie językowe, które uniemożliwia deklarowanie interfejsów i elementów członkowskich interfejsów partial.

Decyzja: Tak. Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface.

Main w interfejsie? (zamknięte)

otwarty problem: Czy metoda static Main w interfejsie jest kandydatem do punktu wejścia programu?

Decyzja: Tak. Zobacz https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#main-in-an-interface.

Potwierdzanie zamiaru obsługi publicznych metod innych niż wirtualne (zamknięte)

Czy możemy potwierdzić (lub odwrócić) naszą decyzję o zezwoleniach na metody publiczne niewirtualne w interfejsie?

interface IA
{
    public sealed void M() { }
}

Semi-Closed Zagadnienie: (2017-04-18) Uważamy, że będzie to przydatne, ale wrócimy do tego. To jest blokada w modelu mentalnym.

Decyzja: Tak. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#confirm-that-we-support-public-non-virtual-methods.

Czy override w interfejsie wprowadza nowy członek? (zamknięte)

Istnieje kilka sposobów, aby zaobserwować, czy deklaracja zastąpienia wprowadza nowy element, czy nie.

interface IA
{
    void M(int x) { }
}
interface IB : IA
{
    override void M(int y) { } // 'override' not permitted
}
interface IC : IB
{
    static void M2()
    {
        M(y: 3); // permitted? Decision: No.
    }
    override void IB.M(int z) { } // permitted? What does it override? Decision: No.
}

otwarte zagadnienie: Czy deklaracja zastąpienia w interfejsie wprowadza nowego członka? (zamknięte)

W klasie zastępowanie metody jest "widoczne" w pewnym sensie. Na przykład nazwy jego parametrów mają pierwszeństwo przed nazwami parametrów w metodzie zastąpionej. Możliwe jest zduplikowanie tego zachowania w interfejsach, ponieważ zawsze istnieje najbardziej konkretne przesłonięcie. Ale czy chcemy zduplikować to zachowanie?

Ponadto można "przesłonić" metodę zastąpienia? Dyskusyjny

decyzja: słowo kluczowe override nie jest dozwolone dla członków interfejsu. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member.

Właściwości z prywatnym zakresem dostępu (ograniczone)

Mówimy, że prywatne elementy członkowskie nie są wirtualne, a połączenie wirtualnych i prywatnych jest niedozwolone. Ale co z właściwością z prywatnym akcesorem?

interface IA
{
    public virtual int P
    {
        get => 3;
        private set { }
    }
}

Czy jest to dozwolone? Czy akcesorium set jest tutaj virtual, czy nie? Czy można go zastąpić tam, gdzie jest dostępny? Czy następujące niejawnie implementuje tylko akcesor get?

class C : IA
{
    public int P
    {
        get => 4;
        set { }
    }
}

Czy to jest prawdopodobnie błąd, ponieważ IA.P.set nie jest wirtualny oraz dlatego, że nie jest dostępny?

class C : IA
{
    int IA.P
    {
        get => 4;
        set { } // Decision: Not valid
    }
}

decyzja: Pierwszy przykład wygląda na prawidłowy, podczas gdy ostatni nie. Jest to rozwiązane analogicznie do sposobu, w jaki działa on już w języku C#. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#properties-with-a-private-accessor

Wywołania interfejsu podstawowego, runda 2 (zamknięta)

Nie zostało to zaimplementowane w języku C# 8.

Nasze poprzednie "rozwiązanie" do obsługi wywołań bazowych nie zapewnia wystarczającej wyrazistości. Okazuje się, że w języku C# i CLR, w przeciwieństwie do języka Java, należy określić zarówno interfejs zawierający deklarację metody, jak i lokalizację implementacji, którą chcesz wywołać.

Proponuję następującą składnię dla wywołań bazowych w interfejsach. Nie jestem w nim zakochany, ale ilustruje to, co każda składnia musi być w stanie wyrazić:

interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
    void I1.M()
    {
        base<I3>(I1).M(); // calls I3's implementation of I1.M
        base<I4>(I1).M(); // calls I4's implementation of I1.M
    }
    void I2.M()
    {
        base<I3>(I2).M(); // calls I3's implementation of I2.M
        base<I4>(I2).M(); // calls I4's implementation of I2.M
    }
}

Jeśli nie ma niejednoznaczności, możesz napisać go bardziej po prostu

interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I4 : I1 { void I1.M() { } }
interface I5 : I3, I4
{
    void I1.M()
    {
        base<I3>.M(); // calls I3's implementation of I1.M
        base<I4>.M(); // calls I4's implementation of I1.M
    }
}

Lub

interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3
{
    void I1.M()
    {
        base(I1).M(); // calls I3's implementation of I1.M
    }
    void I2.M()
    {
        base(I2).M(); // calls I3's implementation of I2.M
    }
}

Lub

interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I5 : I3
{
    void I1.M()
    {
        base.M(); // calls I3's implementation of I1.M
    }
}

decyzja: podjęto decyzję o base(N.I1<T>).M(s), przyznając, że jeśli mamy powiązanie wywołania, później może wystąpić problem. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-14.md#default-interface-implementations

Ostrzeżenie dotyczące struktury, która nie implementuje metody domyślnej? (zamknięte)

@vancem twierdzi, że powinniśmy poważnie rozważyć utworzenie ostrzeżenia, jeśli deklaracja typu wartości nie zastąpi niektórych metod interfejsu, nawet jeśli odziedziczy implementację tej metody z interfejsu. Ponieważ powoduje boks i podważa ograniczone wywołania.

decyzja: Wydaje się, że jest to coś bardziej odpowiedniego dla analizatora. Wydaje się również, że to ostrzeżenie może być hałaśliwe, ponieważ zostanie wyzwolony, nawet jeśli domyślna metoda interfejsu nigdy nie zostanie wywołana i nigdy nie wystąpi boks. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#warning-for-struct-not-implementing-default-method

Konstruktory statyczne interfejsu (zamknięte)

Kiedy są uruchamiane konstruktory statyczne interfejsu? Obecny szkic CLI proponuje, że do tego dochodzi, gdy uzyskuje się dostęp do pierwszej metody statycznej lub pola. Jeśli nie ma żadnego z tych, to może nigdy nie zostać uruchomione?

[2018-10-09 Zespół CLR proponuje "Odzwierciedlenie tego, co robimy dla typów wartości (sprawdzanie cctor przy dostępie do każdej metody instancji)"]

Decision: Konstruktory statyczne są również uruchamiane we wpisie do metod wystąpień, jeśli konstruktor statyczny nie był beforefieldinit, w którym przypadku konstruktory statyczne są uruchamiane przed uzyskaniem dostępu do pierwszego pola statycznego. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#when-are-interface-static-constructors-run

Spotkania projektowe

2017-03-08 Notatki ze spotkania LDM2017-03-21 Notatki ze spotkania LDM2017-03-23 spotkanie "Zachowanie CLR dla domyślnych metod interfejsowych"2017-04-05 Notatki ze spotkania LDM2017-04-11 Notatki ze spotkania LDM2017-04-18 Notatki ze spotkania LDM2017-04-19 Notatki ze spotkania LDM2017-05-17 Notatki ze spotkania LDM2017-05-31 Notatki ze spotkania LDM2017-06-14 Notatki ze spotkania LDM2018-10-17 Notatki ze spotkania LDM2018-11-14 Notatki ze spotkania LDM