Sdílet prostřednictvím


Ukazatele funkcí

Poznámka

Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.

Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v příslušných poznámkách ze schůzky návrhu jazyka (LDM) .

Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .

Shrnutí

Tento návrh poskytuje jazykové konstrukce, které v současné době v jazyce C# zpřístupňují opcode IL, které momentálně nemohou být efektivně přístupné nebo vůbec: ldftn a calli. Tyto opkódy IL můžou být důležité v kódu s vysokým výkonem a vývojáři potřebují efektivní způsob, jak k nim získat přístup.

Motivace

Motivace a pozadí této funkce jsou popsány v následujícím problému (stejně jako potenciální implementace funkce):

dotnet/csharplang#191

Toto je alternativní návrh návrhu vnitřních objektů kompilátoru

Podrobný návrh

Ukazatele na funkce

Jazyk umožní deklaraci ukazatelů funkce pomocí syntaxe delegate*. Úplná syntaxe je podrobně popsaná v další části, ale má se podobat syntaxi používané Func a deklarací typu Action.

unsafe class Example
{
    void M(Action<int> a, delegate*<int, void> f)
    {
        a(42);
        f(42);
    }
}

Tyto typy jsou reprezentovány pomocí typu ukazatele funkce, jak je uvedeno v ECMA-335. To znamená vyvolání delegate* použije calli, kdy vyvolání delegate použije callvirt v metodě Invoke. Syntakticky je vyvolání pro obě konstrukce stejné.

Definice ukazatelů metody ECMA-335 zahrnuje konvenci volání jako součást podpisu typu (oddíl 7.1). Výchozí konvence volání bude managed. Nespravované konvence volání je možné zadat vložením klíčového slova unmanaged po syntaxi delegate*, což bude používat výchozí nastavení platformy modulu runtime. Konkrétní nespravované konvence je pak možné zadat v hranatých závorkách klíčového slova unmanaged zadáním libovolného typu začínajícího CallConv v oboru názvů System.Runtime.CompilerServices a ponecháním předpony CallConv. Tyto typy musí pocházet z základní knihovny programu a sada platných kombinací je závislá na platformě.

//This method has a managed calling convention. This is the same as leaving the managed keyword off.
delegate* managed<int, int>;

// This method will be invoked using whatever the default unmanaged calling convention on the runtime
// platform is. This is platform and architecture dependent and is determined by the CLR at runtime.
delegate* unmanaged<int, int>;

// This method will be invoked using the cdecl calling convention
// Cdecl maps to System.Runtime.CompilerServices.CallConvCdecl
delegate* unmanaged[Cdecl] <int, int>;

// This method will be invoked using the stdcall calling convention, and suppresses GC transition
// Stdcall maps to System.Runtime.CompilerServices.CallConvStdcall
// SuppressGCTransition maps to System.Runtime.CompilerServices.CallConvSuppressGCTransition
delegate* unmanaged[Stdcall, SuppressGCTransition] <int, int>;

Převody mezi typy delegate* se provádějí na základě jejich signatury, včetně konvence volání.

unsafe class Example {
    void Conversions() {
        delegate*<int, int, int> p1 = ...;
        delegate* managed<int, int, int> p2 = ...;
        delegate* unmanaged<int, int, int> p3 = ...;

        p1 = p2; // okay p1 and p2 have compatible signatures
        Console.WriteLine(p2 == p1); // True
        p2 = p3; // error: calling conventions are incompatible
    }
}

Typ delegate* je typ ukazatele, což znamená, že má všechny možnosti a omezení standardního typu ukazatele:

  • Platné pouze v kontextu unsafe.
  • Metody, které obsahují parametr delegate* nebo návratový typ, lze volat pouze z unsafe kontextu.
  • Nelze převést na object.
  • Nelze použít jako obecný argument.
  • Lze implicitně převést delegate* na void*.
  • Lze explicitně převést z void* na delegate*.

Omezení:

  • Vlastní atributy nelze použít na delegate* ani na žádný z jeho prvků.
  • Parametr delegate* nelze označit jako params
  • Typ delegate* má všechna omezení normálního typu ukazatele.
  • Aritmetika ukazatele nelze provést přímo u typů ukazatelů funkce.

Syntaxe ukazatele funkce

Syntaxe ukazatele na celou funkci je reprezentována následující gramatikou:

pointer_type
    : ...
    | funcptr_type
    ;

funcptr_type
    : 'delegate' '*' calling_convention_specifier? '<' funcptr_parameter_list funcptr_return_type '>'
    ;

calling_convention_specifier
    : 'managed'
    | 'unmanaged' ('[' unmanaged_calling_convention ']')?
    ;

unmanaged_calling_convention
    : 'Cdecl'
    | 'Stdcall'
    | 'Thiscall'
    | 'Fastcall'
    | identifier (',' identifier)*
    ;

funptr_parameter_list
    : (funcptr_parameter ',')*
    ;

funcptr_parameter
    : funcptr_parameter_modifier? type
    ;

funcptr_return_type
    : funcptr_return_modifier? return_type
    ;

funcptr_parameter_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

funcptr_return_modifier
    : 'ref'
    | 'ref readonly'
    ;

Pokud není k dispozici žádná calling_convention_specifier, výchozí hodnota je managed. Přesné kódování metadat calling_convention_specifier a platnost identifierv unmanaged_calling_convention je popsáno v reprezentaci konvencí pro volání.

delegate int Func1(string s);
delegate Func1 Func2(Func1 f);

// Function pointer equivalent without calling convention
delegate*<string, int>;
delegate*<delegate*<string, int>, delegate*<string, int>>;

// Function pointer equivalent with calling convention
delegate* managed<string, int>;
delegate*<delegate* managed<string, int>, delegate*<string, int>>;

Převody ukazatelů funkce

V nebezpečném kontextu je sada dostupných implicitních převodů (implicitní převody) rozšířena tak, aby zahrnovala následující implicitní převody ukazatelů:

  • existující převody - (§23.5)
  • Od funcptr_typeF0 k jinému funcptr_typeF1, pokud jsou pravdivé všechny následující skutečnosti:
    • F0 a F1 mají stejný počet parametrů a každý parametr D0n v F0 má stejný ref, outnebo modifikátory in jako odpovídající D1n parametru v F1.
    • Pro každý parametr hodnoty (parametr bez ref, outnebo modifikátoru in), převod identity, implicitní převod odkazu nebo implicitní převod ukazatele existuje z typu parametru v F0 na odpovídající typ parametru v F1.
    • Pro každý parametr ref, outnebo in je typ parametru v F0 stejný jako odpovídající typ parametru v F1.
    • Pokud je návratový typ podle hodnoty (bez ref nebo ref readonly), existuje identita, implicitní odkaz nebo implicitní převod ukazatele z návratového typu F1 na návratový typ F0.
    • Pokud je návratový typ pomocí odkazu (ref nebo ref readonly), pak návratový typ a modifikátory ref u F1 jsou stejné jako návratový typ a modifikátory ref u F0.
    • Konvence volání F0 je stejná jako konvence volání F1.

Povolit adresní metody cíli

Skupiny metod budou nyní povoleny jako argumenty pro adresu výrazu. Typ takového výrazu bude delegate* s ekvivalentním podpisem cílové metody a spravovanou konvencí volání.

unsafe class Util {
    public static void Log() { }

    void Use() {
        delegate*<void> ptr1 = &Util.Log;

        // Error: type "delegate*<void>" not compatible with "delegate*<int>";
        delegate*<int> ptr2 = &Util.Log;
   }
}

V nezabezpečeném kontextu je metoda M kompatibilní s typem ukazatele funkce F, pokud jsou splněny všechny následující podmínky:

  • M a F mají stejný počet parametrů a každý parametr v M má stejný ref, outnebo modifikátory in jako odpovídající parametr v F.
  • Pro každý parametr hodnoty (parametr bez ref, outnebo modifikátoru in), převod identity, implicitní převod odkazu nebo implicitní převod ukazatele existuje z typu parametru v M na odpovídající typ parametru v F.
  • Pro každý parametr ref, outnebo in je typ parametru v M stejný jako odpovídající typ parametru v F.
  • Pokud je návratový typ podle hodnoty (bez ref nebo ref readonly), existuje identita, implicitní odkaz nebo implicitní převod ukazatele z návratového typu F na návratový typ M.
  • Pokud je návratový typ odkazem (ref nebo ref readonly), pak návratový typ a modifikátory ref u F jsou stejné jako návratový typ a modifikátory ref u M.
  • Konvence volání M je stejná jako konvence volání F. To zahrnuje bit konvence volání i všechny příznaky konvence volání zadané v nespravovaném identifikátoru.
  • M je statická metoda.

V nebezpečném kontextu existuje implicitní převod z výrazu adresa, jehož cílem je skupina metod E, na kompatibilní typ ukazatele na funkci F, pokud E obsahuje alespoň jednu metodu, která je v normální formě použitelná pro seznam argumentů vytvořený pomocí typů a modifikátorů parametrů F, jak je popsáno dále.

  • Jedna metoda M je vybrána odpovídající metodě vyvolání formuláře E(A) s následujícími úpravami:
    • Seznam argumentů A je seznam výrazů, z nichž každý je klasifikován jako proměnná a má typ a modifikátor (ref, outnebo in), které odpovídají seznamu parametru funkce funcptr_parameter_list v rámci F.
    • Kandidátské metody jsou pouze metody, které jsou použitelné ve své normální podobě, nikoli v rozšířené podobě.
    • Kandidátské metody jsou pouze metody, které jsou statické.
  • Pokud algoritmus řešení přetížení vytvoří chybu, dojde k chybě v době kompilace. Jinak algoritmus vytvoří jednu nejlepší metodu M se stejným počtem parametrů jako F a převod se považuje za existující.
  • Vybraná metoda M musí být kompatibilní (jak je definováno výše) s typem ukazatele funkce F. V opačném případě dojde k chybě kompilace.
  • Výsledkem převodu je ukazatel funkce typu F.

To znamená, že vývojáři můžou záviset na pravidlech překladu přetížení, která fungují ve spojení s operátorem address-of:

unsafe class Util {
    public static void Log() { }
    public static void Log(string p1) { }
    public static void Log(int i) { }

    void Use() {
        delegate*<void> a1 = &Log; // Log()
        delegate*<int, void> a2 = &Log; // Log(int i)

        // Error: ambiguous conversion from method group Log to "void*"
        void* v = &Log;
    }
}

Operátor adresy se implementuje pomocí ldftn instrukce.

Omezení této funkce:

  • Platí pouze pro metody označené jako static.
  • V &nelze použít jiné nežstatic místní funkce. Podrobnosti implementace těchto metod nejsou záměrně specifikovány jazykem. To zahrnuje, jestli jsou statické nebo instance, a přesně s jakým podpisem jsou vyvolány.

Operátory pro typy funkčních ukazatelů

Oddíl o výrazech v nebezpečném kódu je upraven takto:

V nebezpečném kontextu je k dispozici několik konstruktů pro práci se všemi _pointer_type_s, které nejsou _funcptr_type_s:

  • Operátor * lze použít k provádění nepřímých ukazatelů (§23.6.2).
  • Operátor -> lze použít pro přístup ke členu struktury ukazatelem (§23.6.3).
  • Operátor [] lze použít k indexování ukazatele (§23.6.4).
  • Operátor & lze použít k získání adresy proměnné (§23.6.5).
  • Operátory ++ a -- lze použít k inkrementování a dekrementování ukazatelů (§23.6.6).
  • Operátory + a - lze použít k provádění aritmetik ukazatele (§23.6.7).
  • Operátory ==, !=, <, >, <=a => lze použít k porovnání ukazatelů (§23.6.8).
  • Operátor stackalloc lze použít k přidělení paměti ze zásobníku volání (§23.8).
  • Příkaz fixed lze použít k dočasné opravě proměnné, aby bylo možné získat její adresu (§23.7).

V nebezpečném kontextu je k dispozici několik konstrukcí pro práci se všemi _funcptr_type_s:

Kromě toho upravíme všechny oddíly v Pointers in expressions tak, aby se zakázaly typy ukazatelů funkce s výjimkou Pointer comparison a The sizeof operator.

Lepší funkční člen

§ 12.6.4.3 Lepší člen funkce bude změněn tak, aby zahrnoval následující řádek:

delegate* je konkrétnější než void*

To znamená, že je možné přetížit void* a delegate* a stále rozumně používat adresu operátoru.

Odvození typu

V nebezpečném kódu jsou v algoritmech odvození typu provedeny následující změny:

Vstupní typy

§12.6.3.4

Přidá se následující:

Pokud E je skupina metod adres a T je typ ukazatele funkce, pak všechny typy parametrů T jsou vstupními typy E s typem T.

Výstupní typy

§12.6.3.5

Přidá se následující:

Pokud E je skupina metod s adresou a T je typ ukazatele na funkci, pak návratový typ T je výstupní datový typ E s typem T.

Odvození výstupního typu

§12.6.3.7

Mezi odrážky 2 a 3 se přidá následující odrážka:

  • Pokud E je skupina metod adresy a T je typ ukazatele funkce s typy parametrů T1...Tk a návratovým typem Tba přetížit rozlišení E s typy T1..Tk vrací jedinou metodu s návratovým typem U, pak dolní mez odvozování je vyroben z U do Tb.

Lepší převod z výrazu

§12.6.4.5

Následující dílčí odrážka se přidá v rámci odrážky 2:

  • V je typ ukazatele funkce delegate*<V2..Vk, V1>, U je typ ukazatele funkce delegate*<U2..Uk, U1>a volací konvence V je shodná s Ua refness Vi je shodná s Ui.

Odvození dolní meze

§12.6.3.10

Do odrážky 3 se přidá následující případ:

  • V je typ ukazatele na funkci delegate*<V2..Vk, V1> a existuje typ ukazatele na funkci delegate*<U2..Uk, U1>, tak, že U je totožný s delegate*<U2..Uk, U1>, a volací konvence V je totožná s U, a refness Vi je totožná s Ui.

První odrážka odvozování z Ui do Vi je upravena takto:

  • Pokud U není typ ukazatele funkce a Ui není známo, že se jedná o typ odkazu, nebo pokud U je typ ukazatele funkce a Ui není známo, že se jedná o typ ukazatele funkce nebo typ odkazu, provede se přesná odvození

Potom se přidá za třetí odrážku odvozování z Ui do Vi:

  • Pokud je Vdelegate*<V2..Vk, V1> pak odvození závisí na i-tém parametru delegate*<V2..Vk, V1>:
    • Pokud V1:
      • Pokud je návrat podle hodnoty, provede se odvození v dolní hranici .
      • Pokud je návrat odkazem, provede se přesné odvození.
    • Pokud V2..Vk:
      • Pokud je parametr předán hodnotou, dojde k odvození horní hranice.
      • Pokud je parametr odkazem, provede se přesné odvození.

Odvození horní hranice

§12.6.3.11

Do odrážky 2 se přidá následující případ:

  • U je typ ukazatele na funkci delegate*<U2..Uk, U1> a V je typ ukazatele na funkci, který je shodný s delegate*<V2..Vk, V1>, a volací konvence U je shodná s V, a charakteristika odkazu Ui je shodná s Vi.

První bod odvozování z Ui do Vi se změní na:

  • Pokud U není typ ukazatele funkce a Ui není známo, že se jedná o typ odkazu, nebo pokud U je typ ukazatele funkce a Ui není známo, že se jedná o typ ukazatele funkce nebo typ odkazu, provede se přesná odvození

Potom se přidá za třetí odrážku odvozování z Ui do Vi:

  • Pokud je Udelegate*<U2..Uk, U1> pak odvození závisí na i-tém parametru delegate*<U2..Uk, U1>:
    • Pokud U1:
      • Pokud je vrácen hodnotou, probíhá odvození horní hranice .
      • Pokud je návrat odkazem, provede se přesné odvození.
    • Pokud U2..Uk:
      • Pokud je parametr podle hodnoty, vytvoří se odvození v dolní hranici.
      • Pokud je parametr odkazem, provede se přesná odvození.

Reprezentace metadat in, outa ref readonly parametrů a návratových typů

Podpisy ukazatelů funkce nemají žádné umístění příznaků parametrů, takže musíme kódovat, zda parametry a návratový typ jsou in, outnebo ref readonly pomocí modreqs.

in

Znovu použijeme System.Runtime.InteropServices.InAttribute, v roli modreq u specifikátoru ref u parametru nebo návratového typu, s následujícím významem:

  • Pokud je použit u specifikátoru ref parametru, je tento parametr považován za in.
  • Pokud je použit na specifikátor ref návratového typu, považuje se návratový typ za ref readonly.

out

Používáme System.Runtime.InteropServices.OutAttribute, použité jako modreq specifikátoru ref u typu parametru, aby to znamenalo, že parametr je out parametr.

Chyby

  • Je chybou použít OutAttribute jako modreq pro návratový typ.
  • Jedná se o chybu použití InAttribute i OutAttribute jako modreq pro typ parametru.
  • Pokud je některý z nich zadán prostřednictvím modoptu, budou ignorovány.

Reprezentace metadat konvencí volání

Konvence volání jsou kódovány v podpisu metody v metadatech kombinací příznaku CallKind v podpisu a nula nebo více modopts na začátku podpisu. ECMA-335 v současné době deklaruje následující prvky v příznaku CallKind:

CallKind
   : default
   | unmanaged cdecl
   | unmanaged fastcall
   | unmanaged thiscall
   | unmanaged stdcall
   | varargs
   ;

Z těchto možností budou ukazatele funkcí v jazyce C# podporovat všechny kromě varargs.

Modul runtime (a nakonec 335) se navíc aktualizuje tak, aby zahrnoval nový CallKind na nových platformách. Tento dokument v současné době nemá formální název, ale tento dokument bude používat unmanaged ext jako zástupný symbol pro nový rozšiřitelný formát konvence volání. Bez modopts unmanaged ext je výchozí konvence volání platformy unmanaged bez hranatých závorek.

Mapování calling_convention_specifier na CallKind

calling_convention_specifier, který je buď vynechán, nebo zadán jako managed, se namapuje na defaultCallKind. Toto je výchozí CallKind jakékoli metody, která není přiřazena UnmanagedCallersOnly.

Jazyk C# rozpozná 4 speciální identifikátory, které se mapují na konkrétní existující nespravované CallKinds z ECMA 335. Aby k tomuto mapování mohlo dojít, musí být tyto identifikátory zadány samostatně, bez dalších identifikátorů a tento požadavek je kódován do specifikace pro unmanaged_calling_conventions. Tyto identifikátory jsou Cdecl, Thiscall, Stdcalla Fastcall, které odpovídají unmanaged cdecl, unmanaged thiscall, unmanaged stdcalla unmanaged fastcall. Pokud je zadáno více než jeden identifer, nebo pokud identifier nepatří mezi speciálně rozpoznané identifikátory, provedeme speciální vyhledávání názvů pro tento identifikátor podle následujících pravidel:

  • Předzálohovali jsme identifier řetězcem CallConv
  • Podíváme se jenom na typy definované v oboru názvů System.Runtime.CompilerServices.
  • Podíváme se jenom na typy definované v základní knihovně aplikace, což je knihovna, která definuje System.Object a nemá žádné závislosti.
  • Podíváme se jenom na veřejné typy.

Pokud vyhledávání proběhne úspěšně na všech identifierzadaných v unmanaged_calling_convention, zakódujeme CallKind jako unmanaged exta zakódujeme každý z vyřešených typů v sadě modoptna počátku podpisu ukazatele na funkci. Upozornění: Tato pravidla znamenají, že uživatelé nemohou přidávat tyto identifierjako předponu k CallConv, protože by to mělo za následek vyhledání CallConvCallConvVectorCall.

Při interpretaci metadat se nejprve podíváme na CallKind. Pokud je to cokoli jiného než unmanaged ext, ignorujeme všechny modoptna návratovém typu pro účely určení konvence volání a použijeme pouze CallKind. Pokud je CallKindunmanaged ext, podíváme se na modopty na začátku deklarace typu ukazatele funkce, provedeme sjednocení všech typů, které splňují následující požadavky:

  • Definice se nachází v základní knihovně, která neodkazuje na žádné jiné knihovny a definuje System.Object.
  • Typ je definován v oboru názvů System.Runtime.CompilerServices.
  • Typ začíná předponou CallConv.
  • Typ je veřejný.

Tyto typy představují typy, které musí být nalezeny při vyhledávání na identifiers v unmanaged_calling_convention při definování typu ukazatele funkce ve zdroji.

Jedná se o chybu, pokud se pokusíte použít ukazatel na funkci s parametrem CallKind nastaveným na unmanaged ext a cílové runtime prostředí tuto vlastnost nepodporuje. To bude určeno vyhledáním přítomnosti System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind konstanty. Pokud je tato konstanta přítomná, modul runtime se považuje za podporu této funkce.

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute je atribut používaný CLR k označení, že metoda by měla být volána s konkrétní konvencí volání. Z tohoto důvodu představujeme následující podporu pro práci s atributem:

  • Jedná se o chybu při přímém volání metody anotované pomocí tohoto atributu z jazyka C#. Uživatelé musí získat ukazatel funkce na metodu a pak tento ukazatel vyvolat.
  • Jedná se o chybu použití atributu na cokoli jiného, než je běžná statická metoda nebo běžná statická místní funkce. Kompilátor jazyka C# označí všechny statické nebo nestatické neběžné metody importované z metadat s tímto atributem jako jazykem nepodporované.
  • Je chybou, když metoda označená atributem má parametr nebo návratový typ, který není unmanaged_type.
  • Jedná se o chybu pro metodu označenou atributem, která má parametry typu, i když jsou tyto parametry typu omezeny na unmanaged.
  • Jedná se o chybu pro metodu v obecném typu, která má být označena atributem.
  • Jedná se o chybu při převodu metody označené atributem na typ delegáta.
  • Jedná se o chybu při zadávání všech typů pro UnmanagedCallersOnly.CallConvs, které nesplňují požadavky na konvenci volání modopts v metadatech.

Při určování konvence volání metody označené platným atributem UnmanagedCallersOnly kompilátor provádí následující kontroly typů zadaných v CallConvs vlastnosti k určení efektivní CallKind a modopt, které by se měly použít k určení konvence volání:

  • Pokud nejsou zadány žádné typy, CallKind je považován za unmanaged ext, bez konvencí volání modoptna začátku typu ukazatele funkce.
  • Je-li zadán jeden typ a tento typ má název CallConvCdecl, CallConvThiscall, CallConvStdcallnebo CallConvFastcall, CallKind je považován za unmanaged cdecl, unmanaged thiscall, unmanaged stdcallnebo unmanaged fastcall, bez konvence volání modopts na začátku typu ukazatele funkce.
  • Pokud je zadáno více typů nebo jediný typ není pojmenován jako jeden ze speciálně zmíněných typů výše, CallKind je považován za unmanaged ext, a sjednocení zadaných typů se bere jako modoptna začátku typu ukazatele funkce.

Kompilátor se pak podívá na tuto efektivní CallKind a modopt kolekci a použije normální pravidla metadat k určení konečné konvence volání typu ukazatele funkce.

Otevřené otázky

Detekce podpory modulu runtime pro unmanaged ext

https://github.com/dotnet/runtime/issues/38135 monitoruje přidání tohoto příznaku. V závislosti na zpětné vazbě z recenze buď využijeme vlastnost uvedenou v problému, nebo použijeme přítomnost UnmanagedCallersOnlyAttribute jako příznak, který určuje, zda běhová prostředí podporují unmanaged ext.

Úvahy

Povolit metody instance

Návrh lze rozšířit tak, aby podporoval metody instancí tím, že využívá EXPLICITTHIS konvenci volání rozhraní příkazového řádku (pojmenované instance v kódu jazyka C#). Tato forma ukazatelů funkce rozhraní příkazového řádku umístí parametr this jako explicitní první parametr syntaxe ukazatele funkce.

unsafe class Instance {
    void Use() {
        delegate* instance<Instance, string> f = &ToString;
        f(this);
    }
}

To je rozumné, ale přidává to do návrhu určitou komplikaci. Zejména proto, že ukazatele funkcí, které se liší podle konvence volání instance a managed, by byly nekompatibilní, i když se oba případy používají k vyvolání spravovaných metod se stejným podpisem jazyka C#. Také v každém uvažovaném případě, kde by bylo užitečné mít podobné řešení, existovalo jednoduché alternativní řešení: použijte static místní funkci.

unsafe class Instance {
    void Use() {
        static string toString(Instance i) => i.ToString();
        delegate*<Instance, string> f = &toString;
        f(this);
    }
}

Nepožadujte nebezpečné při deklaraci

Namísto vyžadování unsafe při každém použití delegate*je vyžadováno pouze v okamžiku, kdy je skupina metod převedena na delegate*. V tomto případě přicházejí do hry základní bezpečnostní problémy (s vědomím, že obsahující sestavení nelze uvolnit, když je hodnota aktivní). Vyžadování unsafe na jiných místech může být považováno za nadměrné.

Takto byl návrh původně zamýšlen. Výsledná jazyková pravidla působila velmi neohrabaně. Skutečnost, že se jedná o hodnotu ukazatele, není možné skrýt a stále se objevovala, a to i bez klíčového slova unsafe. Například převod na object nelze povolit, nemůže být členem classatd. Návrh jazyka C# vyžaduje unsafe pro všechny použití ukazatele, a proto tento návrh následuje za tímto účelem.

Vývojáři budou mít stále možnost prezentovat bezpečné obálky nad delegate* hodnotami stejným způsobem jako dnes pro normální typy ukazatelů. Uvažovat:

unsafe struct Action {
    delegate*<void> _ptr;

    Action(delegate*<void> ptr) => _ptr = ptr;
    public void Invoke() => _ptr();
}

Používání delegátů

Místo abyste použili nový prvek syntaxe delegate*, jednoduše použijte existující typy delegate s následujícím typem *.

Func<object, object, bool>* ptr = &object.ReferenceEquals;

Řešení konvence volání lze realizovat anotací typů delegate pomocí atributu, který určuje hodnotu CallingConvention. Nepřítomnost atributu by indikovala spravovanou konvenci volání.

Kódování v IL je problematické. Podkladová hodnota musí být reprezentována jako ukazatel, ale musí také:

  1. Mít jedinečný typ, který umožňuje přetížení s různými typy funkčních ukazatelů.
  2. Být ekvivalentní pro účely OHI napříč hranicemi sestavení.

Poslední bod je obzvláště problematický. To znamená, že každé sestavení, které používá Func<int>*, musí kódovat ekvivalentní typ v metadatech, i když Func<int>* je definován v nějakém sestavení, které neřídí. Kromě toho jakýkoli jiný typ, který je definován s názvem System.Func<T> v sestavení, které není mscorlib, musí být jiné než verze definovaná v mscorlib.

Jednou z možností, kterou bylo prozkoumáno, bylo emitovat takový ukazatel jako mod_req(Func<int>) void*. To ale nefunguje, protože mod_req nemůže vytvořit vazbu na TypeSpec, a proto nemůže cílit na obecné instance.

Pojmenované ukazatele na funkce

Syntaxe ukazatele funkce může být těžkopádná, zejména v složitých případech, jako jsou vnořené ukazatele funkce. Místo toho, aby vývojáři museli pokaždé zadávat signaturu, by mohl jazyk umožnit pojmenované deklarace ukazatelů na funkce, jak je tomu u delegate.

func* void Action();

unsafe class NamedExample {
    void M(Action a) {
        a();
    }
}

Součástí tohoto problému je, že primitivní rozhraní příkazového řádku nemá názvy, a proto by to byla čistě záležitost jazyka C# a umožnění by vyžadovalo určitou práci s metadaty. To je možné, ale je to značné množství práce. V podstatě vyžaduje, aby jazyk C# měl čistě pro tyto názvy doprovodný prvek k definiční tabulce typů.

Také když byly prozkoumány argumenty pojmenovaných ukazatelů funkcí, zjistili jsme, že by mohly být stejně dobře aplikovány na řadu dalších scénářů. Například by bylo stejně tak pohodlné deklarovat pojmenované ntice, aby se snížila nutnost zadávat úplný podpis ve všech případech.

(int x, int y) Point;

class NamedTupleExample {
    void M(Point p) {
        Console.WriteLine(p.x);
    }
}

Po diskuzi jsme se rozhodli nepovolit pojmenovanou deklaraci delegate* typů. Pokud zjistíme, že je to podle zpětné vazby od zákazníků podstatně potřebné, prozkoumáme řešení pojmenování, které funguje pro ukazatele funkcí, n-tice, generické typy atd. Pravděpodobně to bude v podobě podobné jiným návrhům, jako je úplná podpora typedef v jazyce.

Důležité informace o budoucnosti

statický delegát

To se týká návrhu umožnit deklaraci typů delegate, které mohou odkazovat pouze na členy static. Výhodou je, že takové instance delegate nevyžadují přidělování a lépe si vedou ve scénářích náročných na výkon.

Pokud je funkce ukazatele implementovaná, návrh static delegate nejspíše bude uzavřen. Navrhovaná výhoda této funkce je povaha bez nutnosti přidělování paměti. Nedávná šetření však zjistila, že kvůli odstraňování sestavení není možné toho dosáhnout. Musí existovat silná reference z static delegate na metodu, na kterou odkazuje, aby sestavení nebylo uvolněno z pod ní.

Aby bylo možné udržovat každou instanci static delegate, bylo by nutné přidělit nový popisovač, což neodpovídá cílům návrhu. V některých návrzích bylo možné alokaci amortizovat na jedno přidělení na call-site, ale to bylo trochu složité a nestál za to.

To znamená, že vývojáři se v podstatě musí rozhodnout mezi následujícími kompromisy:

  1. Bezpečnost při demontáži sestavy: z toho vyplývá potřeba přidělení, a proto je delegate již dostatečnou možností.
  2. Není zajištěna bezpečnost při uvolňování sestavy: použijte delegate*. To se dá zabalit do struct, aby se ve zbytku kódu povolilo použití mimo kontext unsafe.