Udostępnij za pośrednictwem


Statyczne abstrakcyjne członki w interfejsach

Nota

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 czempionem: https://github.com/dotnet/csharplang/issues/4436

Streszczenie

Interfejs może określać abstrakcyjne składowe statyczne, które klasy i struktury implementujące muszą następnie zaimplementować jawnie lub niejawnie. Dostęp do członków można uzyskać poprzez parametry typu, które są ograniczone przez interfejs.

Motywacja

Obecnie nie ma możliwości abstrakcjonowania statycznych elementów członkowskich i pisania uogólnionego kodu, który ma zastosowanie w różnych typach definiujących te statyczne elementy członkowskie. Jest to szczególnie problematyczne w przypadku rodzajów składowych, które tylko istnieją w postaci statycznej, zwłaszcza operatory.

Ta funkcja umożliwia ogólne algorytmy dla typów liczbowych reprezentowane przez ograniczenia interfejsu określające obecność danych operatorów. W związku z tym algorytmy można wyrazić pod względem takich operatorów:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
    static Int32 IAddable.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

Składnia

Składniki interfejsu

Funkcja umożliwi zadeklarowanie wirtualnych statycznych członków interfejsu.

Reguły przed C# 11

Przed C# 11 członkowie instancji w interfejsach są domyślnie abstrakcyjni (lub wirtualni, jeśli mają domyślną implementację), ale opcjonalnie mogą mieć modyfikator abstract (lub virtual). Członkowie instancji niewirtualnych muszą być jawnie oznaczeni jako sealed.

Statyczne elementy członkowskie interfejsu są obecnie niejawnie niewirtualne i nie zezwalają na modyfikatory abstract, virtual ani sealed.

Propozycja

Abstrakcyjne statyczne członkowie

Statyczne elementy członkowskie interfejsu inne niż pola mogą również mieć modyfikator abstract. Abstrakcyjne statyczne elementy członkowskie nie mogą mieć ciała (a w przypadku właściwości akcesory nie mogą mieć ciała).

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
    static abstract bool operator ==(T l, T r);
    static abstract bool operator !=(T l, T r);
    static abstract implicit operator T(string s);
    static abstract explicit operator string(T t);
}
Wirtualne statyczne członkowie

Członkowie interfejsu statycznego, inni niż pola, mogą również mieć modyfikator virtual. Wirtualni statyczni członkowie muszą mieć ciało.

interface I<T> where T : I<T>
{
    static virtual void M() {}
    static virtual T P { get; set; }
    static virtual event Action E;
    static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
Jawnie niewirtualne statyczne elementy członkowskie

W celu zapewnienia symetrii z elementami członkowskimi instancji niewirtualnymi, statyczne elementy członkowskie (z wyjątkiem pól) powinny mieć możliwość użycia opcjonalnego modyfikatora sealed, mimo że z natury są niewirtualne.

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}

Implementacja elementów członkowskich interfejsu

Dzisiejsze reguły

Klasy i struktury mogą implementować abstrakcyjne elementy członkowskie wystąpień interfejsów niejawnie lub jawnie. Niejawnie zaimplementowana składowa interfejsu jest normalną (wirtualną lub niewirtualną) deklaracją składowej klasy lub struktury, która po prostu przypadkiem przy okazji zaimplementuje składową interfejsu. Składnik może nawet zostać odziedziczony z klasy bazowej, a przez to nie być obecnym w deklaracji klasy.

Jawnie zaimplementowany element członkowski interfejsu używa kwalifikowanej nazwy do identyfikowania danego elementu członkowskiego interfejsu. Implementacja nie jest bezpośrednio dostępna jako element w klasie lub strukturze, ale tylko za pośrednictwem interfejsu.

Propozycja

W klasach i strukturach nie jest wymagana żadna nowa składnia, aby ułatwić niejawną implementację statycznych składowych interfejsu abstrakcyjnego. Istniejące deklaracje statycznych składowych służą do tego celu.

Jawne implementacje statycznych abstrakcyjnych elementów członkowskich interfejsu używają kwalifikowanej nazwy wraz z modyfikatorem static.

class C : I<C>
{
    string _s;
    public C(string s) => _s = s;
    static void I<C>.M() => Console.WriteLine("Implementation");
    static C I<C>.P { get; set; }
    static event Action I<C>.E // event declaration must use field accessor syntax
    {
        add { ... }
        remove { ... }
    }
    static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
    static bool I<C>.operator ==(C l, C r) => l._s == r._s;
    static bool I<C>.operator !=(C l, C r) => l._s != r._s;
    static implicit I<C>.operator C(string s) => new C(s);
    static explicit I<C>.operator string(C c) => c._s;
}

Semantyka

Ograniczenia operatora

Obecnie wszystkie deklaracje operatorów jednoargumentowych i binarnych mają wymagania dotyczące tego, że co najmniej jeden z ich operandów musi być typu T lub T?, gdzie T jest typem wystąpienia otaczającego typu.

Te wymagania muszą zostać złagodzone, aby ograniczony operand mógł być parametrem typu, który jest liczony jako "typ wystąpienia otaczającego typu".

Aby parametr typu T mógł być uznany za "typ wystąpienia tych, które go otaczają", musi spełniać następujące wymagania:

  • T jest bezpośrednim parametrem typu w interfejsie, w którym występuje deklaracja operatora i
  • T jest bezpośrednio ograniczone przez to, co specyfikacja nazywa "instance type" — tj. interfejs otaczający z własnymi parametrami typu używanymi jako argumenty typu.

Operatory równości i konwersje

Abstrakcyjne/wirtualne deklaracje operatorów == i !=, a także abstrakcyjne/wirtualne deklaracje niejawnych i jawnych operatorów konwersji będą dozwolone w interfejsach. Interfejsy pochodne również będą mogły je implementować.

Dla operatorów == i != co najmniej jeden typ parametru musi być parametrem typu, który jest uznawany za "typ wystąpienia obejmującego typu", zgodnie z definicją w poprzedniej sekcji.

Implementowanie statycznych elementów abstrakcyjnych

Reguły dotyczące tego, kiedy statyczna deklaracja członka w klasie lub strukturze jest uznawana za implementację statycznego abstrakcyjnego członka interfejsu, oraz jakie wymagania mają zastosowanie, gdy tak się dzieje, są takie same jak w przypadku członków instancji.

TBD: W tym miejscu mogą istnieć dodatkowe lub różne reguły, o których jeszcze nie myśleliśmy.

Interfejsy jako argumenty typu

Omówiliśmy problem zgłoszony przez https://github.com/dotnet/csharplang/issues/5955 i postanowiliśmy dodać ograniczenie dotyczące użycia interfejsu jako argumentu typu (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts). Oto ograniczenie, które zostało zaproponowane przez https://github.com/dotnet/csharplang/issues/5955 i zatwierdzone przez LDM.

Interfejs zawierający lub dziedziczący statyczny element członkowski abstrakcyjny/wirtualny, który nie ma najbardziej konkretnej implementacji w interfejsie, nie może być używany jako argument typu. Jeśli wszystkie statyczne elementy członkowskie abstrakcyjne/wirtualne mają najbardziej określoną implementację, interfejs może być używany jako argument typu.

Uzyskiwanie dostępu do statycznych składowych interfejsu abstrakcyjnego

Dostęp do statycznego elementu członkowskiego interfejsu abstrakcyjnego M można uzyskać na parametrze typu T przy użyciu wyrażenia T.M, gdy T jest ograniczany przez interfejs I, a M jest dostępnym statycznym abstrakcyjnym elementem członkowskim I.

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t + T.P;
}

W czasie wykonywania wykorzystywana jest rzeczywista implementacja elementu, która istnieje w rzeczywistym typie podanym jako argument typu.

C c = M<C>(); // The static members of C get called

Ponieważ wyrażenia zapytania są określone jako składnia ponownego zapisywania, język C# umożliwia użycie typu jako źródła zapytania, o ile ma statyczne elementy członkowskie dla używanych operatorów zapytań. Innymi słowy, jeśli składnia pasuje, pozwalamy na to! Uważamy, że to zachowanie nie było zamierzone ani istotne w pierwotnej wersji LINQ i nie chcemy pracować, aby je wspierać na parametrach typu. Jeśli istnieją tam scenariusze, usłyszymy o nich i możemy zdecydować się na to później.

Bezpieczeństwo wariancji §18.2.3.2

Reguły bezpieczeństwa wariancji powinny mieć zastosowanie do podpisów statycznych abstrakcyjnych elementów członkowskich. Należy dostosować dodatek proponowany w https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety

Te ograniczenia nie mają zastosowania do wystąpień typów w deklaracjach statycznych elementów członkowskich.

do

Te ograniczenia nie mają zastosowania do wystąpień typów w deklaracjach niewirtualnych, nie abstrakcyjnych statycznych elementów członkowskich.

§10.5.4 Konwersje niejawne zdefiniowane przez użytkownika

Następujące punkty punktorowe

  • Określ typy S, S₀ i T₀.
    • Jeśli E ma typ, to niech S będzie tym typem.
    • Jeśli S lub T są typami dopuszczającymi wartość null, niech Sᵢ i Tᵢ będą ich typami bazowymi, w przeciwnym razie niech Sᵢ i Tᵢ będą odpowiednio S i T.
    • Jeśli Sᵢ lub Tᵢ są parametrami typu, niech S₀ i T₀ będą ich efektywnymi klasami bazowymi, w przeciwnym razie niech S₀ i T₀ będą odpowiednio Sₓ i Tᵢ.
  • Znajdź zestaw typów, D, z którego zostaną uwzględnione operatory konwersji zdefiniowane przez użytkownika. Ten zestaw składa się z S0 (jeśli S0 jest klasą lub strukturą), klasy bazowe S0 (jeśli S0 jest klasą) i T0 (jeśli T0 jest klasą lub strukturą).
  • Znajdź zestaw odpowiednich operatorów konwersji, zdefiniowanych przez użytkownika i podniesionych, U. Ten zestaw składa się z zdefiniowanych przez użytkownika i podniesionych niejawnych operatorów konwersji zadeklarowanych przez klasy lub struktury w D, które konwertują z typu obejmującego S do typu objętego T. Jeśli U jest pusta, konwersja jest niezdefiniowana i wystąpi błąd czasu kompilacji.

są dostosowywane w następujący sposób:

  • Określ typy S, S₀ i T₀.
    • Jeśli E ma typ, przypisz S ten typ.
    • Jeśli S lub T są typami wartości dopuszczającymi wartość null, niech Sᵢ i Tᵢ będą ich typami bazowymi, w przeciwnym razie niech Sᵢ i Tᵢ będą odpowiednio S i T.
    • Jeśli Sᵢ lub Tᵢ są parametrami typu, niech S₀ i T₀ będą ich efektywnymi klasami bazowymi, w przeciwnym razie niech S₀ i T₀ będą odpowiednio Sₓ i Tᵢ.
  • Znajdź zestaw odpowiednich operatorów konwersji zdefiniowanych przez użytkownika oraz operatorów podniesionych, U.
    • Znajdź zestaw typów, D1, z którego zostaną uwzględnione operatory konwersji zdefiniowane przez użytkownika. Ten zestaw składa się z S0 (jeśli S0 jest klasą lub strukturą), klasy bazowe S0 (jeśli S0 jest klasą) i T0 (jeśli T0 jest klasą lub strukturą).
    • Znajdź zestaw odpowiednich operatorów konwersji zdefiniowanych przez użytkownika i podniesionych, U1. Ten zestaw składa się z zdefiniowanych przez użytkownika i podniesionych niejawnych operatorów konwersji zadeklarowanych przez klasy lub struktury w D1, które konwertują z typu obejmującego S do typu objętego T.
    • Jeśli U1 nie jest pusta, U jest U1. Inaczej
      • Znajdź zestaw typów, D2, z którego zostaną uwzględnione operatory konwersji zdefiniowane przez użytkownika. Ten zestaw składa się z Sᵢefektywnego zestawu interfejsów i ich interfejsów podstawowych (jeśli Sᵢ jest parametrem typu), a Tᵢefektywny zestaw interfejsów (jeśli Tᵢ jest parametrem typu).
      • Znajdź zestaw odpowiednich zdefiniowanych przez użytkownika i podniesionych operatorów konwersji, U2. Ten zestaw składa się z zdefiniowanych przez użytkownika i podniesionych niejawnych operatorów konwersji zadeklarowanych przez interfejsy w D2, które konwertują z typu obejmującego S do typu objętego T.
      • Jeśli U2 nie jest pusta, U jest U2
  • Jeśli U jest pusta, konwersja jest niezdefiniowana i wystąpi błąd czasu kompilacji.

§10.3.9 jawne konwersje zdefiniowane przez użytkownika

Następujące punkty punktorowe

  • Określ typy S, S₀ i T₀.
    • Jeśli E ma typ, niech S będzie tym typem.
    • Jeśli S lub T są typami wartościowymi dopuszczającymi wartość null, niech Sᵢ i Tᵢ będą ich typami bazowymi, w przeciwnym razie niech Sᵢ i Tᵢ będą odpowiednio S i T.
    • Jeśli Sᵢ lub Tᵢ są parametrami typu, niech S₀ i T₀ będą ich efektywnymi klasami bazowymi, w przeciwnym razie niech S₀ i T₀ będą odpowiednio Sᵢ i Tᵢ.
  • Znajdź zestaw typów, D, z którego zostaną uwzględnione operatory konwersji zdefiniowane przez użytkownika. Ten zestaw składa się z S0 (jeśli S0 jest klasą lub strukturą), klasy bazowe S0 (jeśli S0 jest klasą), T0 (jeśli T0 jest klasą lub strukturą), a klasy bazowe T0 (jeśli T0 jest klasą).
  • Znajdź zestaw odpowiednich operatorów konwersji zdefiniowanych przez użytkownika i podniesionych, U. Ten zestaw składa się z zdefiniowanych przez użytkownika i podniesionych niejawnych lub jawnych operatorów konwersji zadeklarowanych przez klasy lub struktury w D, które konwertują z typu obejmującego lub objętego przez S na typ obejmujący lub objęty przez T. Jeśli U jest pusta, konwersja jest niezdefiniowana i wystąpi błąd czasu kompilacji.

są dostosowywane w następujący sposób:

  • Określ typy S, S₀ i T₀.
    • Jeśli E ma typ, niech S będzie tym typem.
    • Jeśli S lub T są typami wartości dopuszczającymi null, niech Sᵢ i Tᵢ będą ich typami bazowymi, w przeciwnym razie niech Sᵢ i Tᵢ będą odpowiednio S i T.
    • Jeśli Sᵢ lub Tᵢ są parametrami typu, niech S₀ i T₀ będą ich efektywnymi klasami bazowymi, w przeciwnym razie niech S₀ i T₀ będą odpowiednio Sᵢ i Tᵢ.
  • Znajdź zestaw odpowiednich zdefiniowanych przez użytkownika i podniesionych operatorów konwersji, U.
    • Znajdź zestaw typów, D1, z którego zostaną uwzględnione operatory konwersji zdefiniowane przez użytkownika. Ten zestaw składa się z S0 (jeśli S0 jest klasą lub strukturą), klasy bazowe S0 (jeśli S0 jest klasą), T0 (jeśli T0 jest klasą lub strukturą), a klasy bazowe T0 (jeśli T0 jest klasą).
    • Znajdź zestaw odpowiednich operatorów konwersji zdefiniowanych przez użytkownika i zniesionych, U1. Ten zbiór składa się z zdefiniowanych przez użytkownika i podniesionych do rangi niejawnych lub jawnych operatorów konwersji zadeklarowanych przez klasy lub struktury w D1, które konwertują z typu obejmującego lub objętego przez S na typ obejmujący lub objęty przez T.
    • Jeśli U1 nie jest pusta, U jest U1. Inaczej
      • Znajdź zestaw typów, D2, z którego zostaną uwzględnione operatory konwersji zdefiniowane przez użytkownika. Ten zestaw składa się z Sᵢefektywnego zestawu interfejsów i ich interfejsów podstawowych (jeśli Sᵢ jest parametrem typu), a Tᵢefektywny zestaw interfejsów i ich interfejsów podstawowych (jeśli Tᵢ jest parametrem typu).
      • Znajdź zestaw odpowiednich operatorów konwersji zdefiniowanych przez użytkownika i podniesionych, U2. Ten zestaw składa się z zdefiniowanych przez użytkownika i podniesionych niejawnych lub jawnych operatorów konwersji zadeklarowanych przez interfejsy w D2, które konwertują z typu obejmowanego lub obejmującego S do typu obejmowanego lub obejmującego przez T.
      • Jeśli U2 nie jest pusta, U jest U2
  • Jeśli U jest pusta, konwersja jest niezdefiniowana i wystąpi błąd czasu kompilacji.

Implementacje domyślne

dodatkową funkcją dla tej propozycji jest umożliwienie statycznym elementom wirtualnym w interfejsach domyślnych implementacji, podobnie jak elementy członkowskie wirtualne/abstrakcyjne wystąpienia.

Jedną z komplikacji jest to, że domyślne implementacje powinny wywoływać inne statyczne wirtualne elementy członkowskie w sposób "wirtualny". Zezwolenie na bezpośrednie wywoływanie członków statycznych, które są wirtualne, na poziomie interfejsu wymagałoby przekazywania ukrytego parametru typu reprezentującego typ "self", na którym rzeczywiście wywołano aktualną metodę statyczną. Wydaje się to skomplikowane, kosztowne i potencjalnie mylące.

Omówiliśmy prostszą wersję, która utrzymuje ograniczenia bieżącej propozycji, że statyczne wirtualne elementy członkowskie mogą wywoływane tylko na parametry typu. Ponieważ interfejsy ze statycznymi członkami wirtualnymi często mają jawny parametr typu reprezentujący typ "self", nie byłoby to dużą stratą: inne statyczne członki wirtualne mogą być po prostu wywoływane dla tego typu "self". Ta wersja jest o wiele prostsza i wydaje się całkiem do zrobienia.

W https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics postanowiliśmy wspierać domyślne implementacje statycznych elementów członkowskich, dostosowując się i rozwijając reguły ustanowione w https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md.

Dopasowywanie wzorca

Biorąc pod uwagę następujący kod, użytkownik może rozsądnie oczekiwać, że wyświetli "True" (tak, jakby wzorzec stałej został zapisany w kodzie źródłowym):

M(1.0);

static void M<T>(T t) where T : INumberBase<T>
{
    Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
    Console.WriteLine((t is int i) && (i is 1)); 
}

Jednakże, ponieważ typ danych wejściowych wzorca nie jest double, stały wzorzec 1 najpierw sprawdzi nadchodzące T względem int. Jest to nieintuicyjne, dlatego jest blokowane do czasu, aż przyszła wersja języka C# doda lepszą obsługę dopasowywania liczbowego do typów pochodnych od INumberBase<T>. W tym celu powiemy, że jawnie rozpoznamy INumberBase<T> jako typ, z którego pochodzą wszystkie "liczby", i zablokujemy wzorzec, jeśli próbujemy dopasować wzorzec stałej liczbowej do typu liczbowego, w jakim nie możemy reprezentować wzorca (tj. parametr typu ograniczony do INumberBase<T>lub typu liczbowego zdefiniowanego przez użytkownika, który dziedziczy z INumberBase<T>).

Formalnie dodamy wyjątek do definicji zgodnej ze wzorcem dla wzorców stałych:

Stały wzorzec sprawdza wartość wyrażenia względem stałej wartości. Stała może być dowolnym wyrażeniem stałym, takim jak literał, nazwa zadeklarowanej zmiennej const lub stała wyliczenia. Jeśli wartość wejściowa nie jest typem otwartym, wyrażenie stałe jest niejawnie konwertowane na typ dopasowanego wyrażenia; Jeśli typ wartości wejściowej nie jest zgodny ze wzorcem z typem wyrażenia stałego, operacja dopasowywania wzorca jest błędem. Jeśli wyrażenie stałe, które jest dopasowywane, jest wartością liczbową, a wartość wejściowa jest typem, który dziedziczy z System.Numerics.INumberBase<T>, i brak jest możliwości stałej konwersji wyrażenia stałego do typu wartości wejściowej, operacja dopasowywania wzorca jest błędem.

Dodamy również podobny wyjątek dla wzorców relacyjnych:

Gdy wejście jest typem, dla którego zdefiniowano odpowiedni wbudowany binarny operator relacyjny, stosowany z wejściem jako lewym operandem, a stałą jako prawym operandem, wynik działania tego operatora jest przyjmowany jako znaczenie wzorca relacyjnego. W przeciwnym razie, konwertujemy dane wejściowe do typu wyrażenia przy użyciu jawnej konwersji do typu dopuszczającego null lub rozpakowującej. Jest to błąd czasu kompilacji, jeśli taka konwersja nie istnieje. Jest to błąd czasu kompilacji, jeśli typ wejściowy jest ograniczony przez parametr typu lub dziedziczący z System.Numerics.INumberBase<T>, a typ wejściowy nie ma wbudowanego odpowiedniego operatora relacyjnego binarnego. Wzorzec jest uznawany za niezgodny, jeśli konwersja nie powiedzie się. Jeśli konwersja powiedzie się, wynikiem operacji dopasowywania wzorca jest ocena wyrażenia e OP v, gdzie e jest przekonwertowanym wejściem, OP jest operatorem relacyjnym, a v jest wyrażeniem stałym.

Wady

  • "Statyczna abstrakcja" to nowa koncepcja, która znacząco wpłynie na zwiększenie koncepcyjnego obciążenia języka C#.
  • Nie jest to tania funkcja do zbudowania. Powinniśmy upewnić się, że warto.

Alternatywy

Ograniczenia strukturalne

Alternatywną metodą byłoby bezpośrednie "ograniczenia strukturalne" i jawne wymaganie obecności określonych operatorów na parametrze typu. Wady tego są: - To musiałoby być zapisywane za każdym razem. Posiadanie nazwanego ograniczenia wydaje się lepsze. - Jest to zupełnie nowy rodzaj ograniczenia, natomiast proponowana funkcja wykorzystuje istniejącą koncepcję ograniczeń interfejsu. - Działałoby to tylko dla operatorów, a inne rodzaje statycznych elementów członkowskich byłyby trudne do wdrożenia.

Nierozwiązane pytania

Statyczne interfejsy abstrakcyjne i klasy statyczne

Aby uzyskać więcej informacji, zobacz https://github.com/dotnet/csharplang/issues/5783 i https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes.

Spotkania projektowe