Dela via


Funktionspekare

Notera

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta LDM-anteckningarna (språkutvecklingsmöte).

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Sammanfattning

Det här förslaget innehåller språkkonstruktioner som exponerar IL-opcodes som för närvarande inte kan nås effektivt, eller alls, i C# idag: ldftn och calli. Dessa IL-opcodes kan vara viktiga i kod med höga prestanda och utvecklare behöver ett effektivt sätt att komma åt dem.

Motivation

Motiveringarna och bakgrunden till den här funktionen beskrivs i följande ärende (liksom en potentiell implementering av funktionen):

dotnet/csharplang#191

Det här är ett alternativt designförslag till kompilatorns inbyggda

Detaljerad design

Funktionspekare

Språket tillåter deklaration av funktionspekare med hjälp av syntaxen delegate*. Den fullständiga syntaxen beskrivs i detalj i nästa avsnitt, men den är avsedd att likna syntaxen som används av Func och Action typdeklarationer.

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

Dessa typer representeras med hjälp av funktionspekartypen enligt beskrivningen i ECMA-335. Det innebär att anrop av en delegate* använder calli där anrop av en delegate använder callvirt på metoden Invoke. Syntaktiskt är dock anropet identiskt för båda konstruktionerna.

ECMA-335-definitionen av metodpekare innehåller anropskonventionen som en del av typsignaturen (avsnitt 7.1). Standardanropskonventionen är managed. Ohanterade anropskonventioner kan anges genom att lägga till ett unmanaged-nyckelord efter delegate*-syntax, vilket använder körningsplattformens standardinställning. Specifika ohanterade konventioner kan sedan anges inom hakparenteser till nyckelordet unmanaged genom att ange vilken typ som helst som börjar med CallConv i System.Runtime.CompilerServices namnrymd, vilket lämnar CallConv prefixet. Dessa typer måste komma från programmets kärnbibliotek och uppsättningen giltiga kombinationer är plattformsberoende.

//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>;

Konverteringar mellan delegate* typer görs baserat på deras signatur, inklusive anropskonventionen.

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
    }
}

En delegate* typ är en pekartyp, vilket innebär att den har alla funktioner och begränsningar för en standardpekartyp:

  • Endast giltigt i en unsafe-kontext.
  • Metoder som innehåller en delegate* parameter eller returtyp kan bara anropas från en unsafe kontext.
  • Det går inte att konvertera till object.
  • Det går inte att använda som ett allmänt argument.
  • Kan implicit konvertera delegate* till void*.
  • Kan explicit konvertera från void* till delegate*.

Inskränkningar:

  • Anpassade attribut kan inte tillämpas på en delegate* eller något av dess element.
  • Det går inte att markera en delegate* parameter som params
  • En delegate* typ har alla begränsningar för en normal pekartyp.
  • Pekarens aritmetik kan inte utföras direkt på funktionspekartyper.

Syntax för funktionspekare

Den fullständiga syntaxen för funktionspekaren representeras av följande grammatik:

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'
    ;

Om ingen calling_convention_specifier anges är standardvärdet managed. Exakt metadatakodning av calling_convention_specifier och vilka identifiersom är giltiga i unmanaged_calling_convention beskrivs i Metadatarepresentation av samtalskonventioner.

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>>;

Konvertering av funktionspekare

I ett osäkert sammanhang utökas uppsättningen med tillgängliga implicita konverteringar (implicita konverteringar) till att omfatta följande implicita pekarkonverteringar:

  • Befintliga konverteringar - (§23.5)
  • Från funcptr_typeF0 till en annan funcptr_typeF1, förutsatt att allt följande är sant:
    • F0 och F1 har samma antal parametrar och varje parameter D0n i F0 har samma ref, outeller in modifierare som motsvarande parameter D1n i F1.
    • För varje värdeparameter (en parameter utan ref, outeller in modifierare) finns det en identitetskonvertering, implicit referenskonvertering eller implicit pekarkonvertering från parametertypen i F0 till motsvarande parametertyp i F1.
    • För varje parameter ref, outeller in är parametertypen i F0 samma som motsvarande parametertyp i F1.
    • Om returtypen är ett värde (ingen ref eller ref readonly) finns det en identitet, implicit referens eller implicit pekarkonvertering från returtypen av F1 till returtypen av F0.
    • Om returtypen är per referens (ref eller ref readonly) är returtypen och ref modifierare för F1 desamma som returtypen och ref modifierare för F0.
    • Anropskonventionen för F0 är densamma som den anropande konventionen för F1.

Tillåt adress-of att rikta in sig på metoder

Metodgrupper kommer nu att tillåtas som argument i ett adressuttryck. Typen av ett sådant uttryck är en delegate* som har motsvarande signatur för målmetoden och en konvention för hanterade anrop:

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;
   }
}

I ett osäkert sammanhang är en metod M kompatibel med en funktionspekartyp F om allt av följande är sant:

  • M och F har samma antal parametrar och varje parameter i M har samma ref, outeller in modifierare som motsvarande parameter i F.
  • För varje värdeparameter (en parameter utan ref, outeller in modifierare) finns det en identitetskonvertering, implicit referenskonvertering eller implicit pekarkonvertering från parametertypen i M till motsvarande parametertyp i F.
  • För varje parameter ref, outeller in är parametertypen i M samma som motsvarande parametertyp i F.
  • Om returtypen är efter värde (ingen ref eller ref readonly) finns det en identitet, implicit referens eller implicit pekarkonvertering från returtypen för F till returtypen M.
  • Om returtypen anges som referens (ref eller ref readonly) är returtypen och ref-modifierarna av F samma som returtypen och ref-modifierarna av M.
  • Anropskonventionen för M är densamma som den anropande konventionen för F. Detta omfattar både anropskonventionsbiten och alla anropskonventionsflaggor som anges i den ohanterade identifieraren.
  • M är en statisk metod.

I ett osäkert sammanhang finns det en implicit konvertering från ett adressuttryck vars mål är en metodgrupp E till en kompatibel funktionspekartyp F om E innehåller minst en metod som är tillämplig i sin normala form på en argumentlista som konstruerats med hjälp av parametertyperna och modifierarna för F, enligt beskrivningen i följande.

  • En enda metod M väljs som motsvarar en metodanrop av formuläret E(A) med följande ändringar:
    • Argumentlistan A är en lista med uttryck, var och en klassificerad som en variabel och med typen och modifieraren (ref, outeller in) för motsvarande funcptr_parameter_list av F.
    • Kandidatmetoderna är endast de metoder som är tillämpliga i sin normala form, inte de som gäller i utökad form.
    • Kandidatmetoderna är bara de metoder som är statiska.
  • Om algoritmen för överbelastningsmatchning genererar ett fel uppstår ett kompileringsfel. Annars genererar algoritmen en enda bästa metod M har samma antal parametrar som F och konverteringen anses finnas.
  • Den valda metoden M måste vara kompatibel (enligt definitionen ovan) med funktionspekartypen F. Annars uppstår ett kompileringsfel.
  • Resultatet av konverteringen är en funktionspekare av typen F.

Det innebär att utvecklare kan vara beroende av regler för överbelastningsmatchning för att fungera tillsammans med adressoperatorn:

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;
    }
}

Adress för operatorn implementeras med hjälp av instruktionen ldftn.

Begränsningar för den här funktionen:

  • Gäller endast för metoder som har markerats som static.
  • Icke-static lokala funktioner kan inte användas i &. Implementeringsinformationen för dessa metoder anges avsiktligt inte av språket. Detta inkluderar om de är statiska jämfört med instanser eller exakt vilken signatur de genereras med.

Operatorer för funktionspekartyper

Avsnittet i osäker kod för uttryck ändras så här:

I ett osäkert sammanhang finns flera konstruktioner tillgängliga för att arbeta med alla _pointer_type_s som inte är _funcptr_type_s.

  • Den * operatorn kan användas för indirekt hantering av pekare (§23.6.2).
  • Operatören -> kan användas för att komma åt en medlem i en struct genom en pekare (§23.6.3).
  • Den [] operatorn kan användas för att indexera en pekare (§23.6.4).
  • Den & operatören kan användas för att hämta adressen till en variabel (§23.6.5).
  • Operatorerna ++ och -- kan användas för att öka och minska pekare (§23.6.6).
  • Operatorerna + och - kan användas för att utföra pekararitmetik (§23.6.7).
  • Operatorerna ==, !=, <, >, <=och => kan användas för att jämföra pekare (§23.6.8).
  • Den stackalloc operatorn kan användas för att allokera minne i anropsstacken (§23.8).
  • Den fixed instruktionen kan användas för att tillfälligt åtgärda en variabel så att dess adress kan erhållas (§23.7).

I ett osäkert sammanhang är flera konstruktioner tillgängliga för drift på alla _funcptr_type_s:

Dessutom ändrar vi alla avsnitt i Pointers in expressions för att förbjuda funktionspekartyper, förutom Pointer comparison och The sizeof operator.

Bättre funktionsmedlem

§12.6.4.3 Bättre funktionsmedlem kommer att ändras för att inkludera följande rad:

En delegate* är mer specifik än void*

Det innebär att det är möjligt att överbelasta void* och en delegate* och fortfarande använda adressoperatorn på ett förnuftigt sätt.

Typinferens

I osäker kod görs följande ändringar i typinferensalgoritmerna:

Indatatyper

§12.6.3.4

Följande läggs till:

Om E är en metodgruppsadress och T är en funktionspekartyp är alla parametertyper av T indatatyper av E med typen T.

Utdatatyper

§12.6.3.5

Följande läggs till:

Om E är en metodgruppsadress och T är en typ av funktionspekare är returtypen T en utdatatyp av E med typen T.

Slutsatsdragningar för utdatatyp

§12.6.3.7

Följande punkt läggs till mellan punkterna 2 och 3:

  • Om E är en metodgruppsadress och T är en funktionspekartyp med parametertyper T1...Tk och returtyp Tboch överlagringsmatchning av E med typerna T1..Tk ger en enda metod med returtyp U, görs en lägre bindningsslutsats från U till Tb.

Bättre konvertering från uttryck

§12.6.4.5

Följande underpunkt läggs till som ett ärende i punkt 2:

  • V är en funktionspekartyp delegate*<V2..Vk, V1> och U är en funktionspekartyp delegate*<U2..Uk, U1>, och den anropande konventionen för V är identisk med U, och referensen för Vi är identisk med Ui.

Slutsatser med nedre gräns

§12.6.3.10

Följande fall läggs till i punkt 3:

  • V är en funktionspekartyp delegate*<V2..Vk, V1> och det finns en funktionspekartyp delegate*<U2..Uk, U1> så att U är identisk med delegate*<U2..Uk, U1>, och anropskonventionen för V är identisk med U, och refnessen för Vi är identisk med Ui.

Den första inferensen från Ui till Vi ändras till:

  • Om U inte är en funktionspekartyp och Ui inte är känd för att vara en referenstyp, eller om U är en funktionspekartyp och Ui inte är känd för att vara en funktionspekartyp eller en referenstyp, görs en exakt slutsatsdragning görs

Sedan läggs till efter den tredje punkten av slutsatsdragning från Ui till Vi:

  • Om V är delegate*<V2..Vk, V1> beror annars slutsatsdragningen på parametern i-th i delegate*<V2..Vk, V1>:
    • Om V1:
      • Om returen är efter värde görs en inferens av nedre gräns.
      • Om det returneras som en referens görs en exakt slutsats.
    • Om V2..Vk:
      • Om parametern är efter värde görs en övre gränsinferens.
      • Om parametern är som referens görs en exakt slutsatsdragning.

Slutsatsdragningar med övre gräns

§12.6.3.11

Följande fall läggs till i punkt 2:

  • U är en funktionspekartyp delegate*<U2..Uk, U1> och V är en typ av funktionspekare som är identisk med delegate*<V2..Vk, V1>, och anropskonventionen för U är identisk med V, och referensen för Ui är identisk med Vi.

Den första inferensen från Ui till Vi ändras till:

  • Om U inte är en funktionspekartyp och Ui inte är känd för att vara en referenstyp, eller om U är en funktionspekartyp och Ui inte är känd för att vara en funktionspekartyp eller en referenstyp, görs en exakt slutsatsdragning görs

Sedan lagt till efter den tredje punkten för slutsats från Ui till Vi:

  • Om U är delegate*<U2..Uk, U1> beror annars slutsatsdragningen på parametern i-th i delegate*<U2..Uk, U1>:
    • Om U1:
      • Om returen är efter värde görs en övre gränsinferens.
      • Om returen är genom referens görs en exakt slutsatsdragning.
    • Om U2..Uk:
      • Om parametern är per värde skapas en slutsats med lägre gräns.
      • Om parametern är som referens görs en exakt slutsatsdragning.

Metadatarepresentation av in, outoch ref readonly parametrar och returtyper

Funktionspekarsignaturer har ingen plats för parameterflaggor, så vi måste koda om parametrar och returtypen är in, outeller ref readonly med hjälp av modreqs.

in

Vi återanvänder System.Runtime.InteropServices.InAttribute, som används som en modreq till referensspecificeraren för en parameter eller returtyp, för att betyda följande:

  • Om den tillämpas på en parameter ref-specificerare behandlas den här parametern som in.
  • Om den tillämpas på referensspecificeraren för returtyp behandlas returtypen som ref readonly.

out

Vi använder System.Runtime.InteropServices.OutAttribute, som används som en modreq för referensspecificeraren för en parametertyp, vilket innebär att parametern är en out parameter.

Fel

  • Det är ett fel att tillämpa OutAttribute som en modreq på en returtyp.
  • Det är ett fel att tillämpa både InAttribute och OutAttribute som en modreq på en parametertyp.
  • Om någon av dem anges via modopt ignoreras de.

Metadatarepresentation av samtalskonventioner

Anropskonventioner kodas i en metodsignatur i metadata genom en kombination av flaggan CallKind i signaturen och noll eller fler modopti början av signaturen. ECMA-335 deklarerar för närvarande följande element i flaggan CallKind:

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

Av dessa stöder funktionspekare i C# alla utom varargs.

Dessutom uppdateras körmiljön (och slutligen 335) för att lägga till en ny CallKind på nya plattformar. Detta har för närvarande inget formellt namn, men det här dokumentet använder unmanaged ext som platshållare för att stå för det nya utökningsbara anropskonventionformatet. Utan modopts är unmanaged ext standardanropskonventionen för plattformen, unmanaged utan hakparenteser.

Mappa calling_convention_specifier till en CallKind

En calling_convention_specifier som utelämnas, eller anges som managed, mappas till defaultCallKind. Detta är standard CallKind för alla metoder som inte tillskrivs UnmanagedCallersOnly.

C# identifierar 4 särskilda identifierare som mappar till specifika befintliga ohanterade CallKindfrån ECMA 335. För att den här mappningen ska ske måste dessa identifierare anges på egen hand, utan några andra identifierare, och det här kravet kodas i specifikationen för unmanaged_calling_conventions. Dessa identifierare är Cdecl, Thiscall, Stdcalloch Fastcall, som motsvarar unmanaged cdecl, unmanaged thiscall, unmanaged stdcallrespektive unmanaged fastcall. Om fler än en identifer har angetts, eller om den enda identifier inte är av de särskilt identifierade identifierarna, utför vi ett särskilt namnsökning på identifieraren med följande regler:

  • Vi förbereder identifier med strängen CallConv
  • Vi tittar bara på typer som definierats i System.Runtime.CompilerServices namnrymd.
  • Vi tittar bara på typer som definierats i programmets kärnbibliotek, vilket är biblioteket som definierar System.Object och inte har några beroenden.
  • Vi tittar bara på offentliga typer.

Om sökningen lyckas på alla identifiersom anges i en unmanaged_calling_conventionkodar vi CallKind som unmanaged extoch kodar var och en av de lösta typerna i uppsättningen med modopts i början av funktionspekarsignaturen. Observera att dessa regler innebär att användarna inte kan prefix dessa identifiers med CallConv, eftersom det resulterar i att leta upp CallConvCallConvVectorCall.

När vi tolkar metadata tittar vi först på CallKind. Om det är något annat än unmanaged extignorerar vi alla modoptpå returtypen för att fastställa anropskonventionen och använder endast CallKind. Om CallKind är unmanaged exttittar vi på modopterna i början av funktionspekartypen och bildar en union av alla typer som uppfyller följande krav:

  • Definieras i kärnbiblioteket, vilket är biblioteket som inte refererar till några andra bibliotek och definierar System.Object.
  • Typen definieras i namnområdet System.Runtime.CompilerServices.
  • Typen börjar med prefixet CallConv.
  • Typen är offentlig.

Dessa representerar de typer som måste hittas när du utför sökning på identifiers i en unmanaged_calling_convention när du definierar en funktionspekartyp i källan.

Det är ett fel att försöka använda en funktionspekare med en CallKind av unmanaged ext om målmiljön inte stöder egenskapen. Detta bestäms genom att söka efter förekomsten av System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind konstant. Om den här konstanten finns anses körmiljön stödja funktionen.

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute är ett attribut som används av CLR för att ange att en metod ska anropas med en specifik anropskonvention. Därför introducerar vi följande stöd för att arbeta med attributet:

  • Det är ett fel att direkt anropa en metod som kommenterats med det här attributet från C#. Användarna måste hämta en funktionspekare till metoden och sedan anropa den pekaren.
  • Det är ett fel att tillämpa attributet på något annat än en vanlig statisk metod eller vanlig statisk lokal funktion. C#-kompilatorn markerar alla icke-statiska eller statiska icke-vanliga metoder som importerats från metadata med det här attributet som inte stöds av språket.
  • Det är ett fel om en metod är markerad med attributet och har en parameter eller returtyp som inte är en unmanaged_type.
  • Det är ett fel för en metod som har markerats med attributet för att ha typparametrar, även om dessa typparametrar är begränsade till unmanaged.
  • Det är ett fel för en metod i en generisk typ att markeras med attributet.
  • Det är ett fel att konvertera en metod som markerats med attributet till en delegerad typ.
  • Det är ett fel att ange några typer för UnmanagedCallersOnly.CallConvs som inte uppfyller kraven för att anropa konventionen modopts i metadata.

När du fastställer anropskonventionen för en metod som har markerats med ett giltigt UnmanagedCallersOnly attribut, utför kompilatorn följande kontroller av de typer som anges i egenskapen CallConvs för att fastställa den effektiva CallKind och modoptsom ska användas för att fastställa anropskonventionen:

  • Om inga typer anges behandlas CallKind som unmanaged ext, utan anropskonvention modopts i början av funktionspekartypen.
  • Om det finns en angiven typ och den typen heter CallConvCdecl, CallConvThiscall, CallConvStdcalleller CallConvFastcallbehandlas CallKind som unmanaged cdecl, unmanaged thiscall, unmanaged stdcallrespektive unmanaged fastcall, utan anropskonvention modopts i början av funktionspekartypen.
  • Om flera typer anges eller om den enskilda typen inte heter någon av de särskilt kallade typerna ovan behandlas CallKind som unmanaged ext, med union av de angivna typerna som behandlas som modopts i början av funktionspekartypen.

Kompilatorn tittar sedan på den här effektiva CallKind och modopt samling och använder normala metadataregler för att fastställa den sista anropande konventionen för funktionspekartypen.

Öppna frågor

Identifiera körningsstöd för unmanaged ext

https://github.com/dotnet/runtime/issues/38135 spårar hur du lägger till den här flaggan. Beroende på feedbacken från granskningen använder vi antingen egenskapen som anges i problemet, eller använder vi närvaron av UnmanagedCallersOnlyAttribute som den flagga som avgör om körtiderna stöder unmanaged ext.

Överväganden

Tillåt instansmetoder

Förslaget kan utökas för att stödja instansmetoder genom att dra nytta av EXPLICITTHIS CLI-anropskonvention (med namnet instance i C#-kod). Den här formen av CLI-funktionspekare placerar parametern this som en explicit första parameter i syntaxen för funktionspekaren.

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

Detta är bra men ger en viss komplikation till förslaget. Särskilt eftersom funktionspekare som skiljer sig från anropskonventionen instance och managed skulle vara inkompatibla även om båda fallen används för att anropa hanterade metoder med samma C#-signatur. I alla fall där detta skulle vara värdefullt att ha, fanns det en enkel lösning: använd en lokal funktion med static.

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

Kräv inte osäker vid deklaration

I stället för att kräva unsafe vid varje användning av en delegate*behöver du bara den vid den tidpunkt då en metodgrupp konverteras till en delegate*. Det är här de viktigaste säkerhetsfrågorna spelar in (med vetskapen om att den innehållande modulen inte kan avlägsnas medan värdet bibehålls). Att kräva unsafe på de andra platserna kan ses som överdrivet.

Så här var designen ursprungligen avsedd. Men de resulterande språkreglerna kändes mycket besvärliga. Det är omöjligt att dölja det faktum att detta är ett pekarvärde och det fortsatte att synas även utan nyckelordet unsafe. Konverteringen till object kan till exempel inte tillåtas, den kan inte vara medlem i en classosv. ... C#-designen är att kräva unsafe för alla pekare och därför följer den här designen det.

Utvecklare kommer fortfarande att kunna presentera en säker wrapper ovanpå delegate* värden på samma sätt som de gör för vanliga pekartyper idag. Överväga:

unsafe struct Action {
    delegate*<void> _ptr;

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

Använda ombud

I stället för att använda ett nytt syntaxelement delegate*använder du bara befintliga delegate typer med en * som följer typen:

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

Hantering av anropskonvention kan göras genom att kommentera de delegate typerna med ett attribut som anger ett CallingConvention värde. Avsaknaden av ett attribut skulle innebära en hanterad anropskonvention.

Det är problematiskt att koda detta i IL. Det underliggande värdet måste representeras som en pekare, men det måste också:

  1. Ha en unik typ för att tillåta överbelastningar med olika funktionspekartyper.
  2. Vara ekvivalent för OHI-ändamål över sammansättningsgränser.

Den sista punkten är särskilt problematisk. Det innebär att varje assembly som använder Func<int>* måste koda en motsvarande typ i metadata, även om Func<int>* definieras i en assembly som de inte kontrollerar. Dessutom måste alla andra typer som definieras med namnet System.Func<T> i en sammansättning som inte är mscorlib skilja sig från den version som definierats i mscorlib.

Ett alternativ som utforskades var att sända ut en sådan pekare som mod_req(Func<int>) void*. Detta fungerar dock inte som en mod_req inte kan binda till en TypeSpec och kan därför inte rikta in sig på generiska instansieringar.

Namngivna funktionspekare

Syntaxen för funktionspekaren kan vara besvärlig, särskilt i komplexa fall som kapslade funktionspekare. I stället för att låta utvecklare skriva ut signaturen varje gång skulle språket kunna tillåta namngivna deklarationer av funktionspekare som görs med delegate.

func* void Action();

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

En del av problemet här är att den underliggande CLI-primitiven inte har namn, därför skulle detta vara en ren C#-uppfinning och kräva lite metadataarbete för att aktivera. Det är genomförbart men handlar mycket om arbete. Det kräver i princip att C# har en följeslagare till tabellen type def enbart för dessa namn.

När argumenten för namngivna funktionspekare undersöktes upptäckte vi också att de kunde tillämpas lika bra på ett antal andra scenarier. Det skulle till exempel vara lika praktiskt att deklarera namngivna tupplar för att minska behovet av att skriva ut den fullständiga signaturen i alla fall.

(int x, int y) Point;

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

Efter diskussionen beslutade vi att inte tillåta namngiven deklaration av delegate* typer. Om vi upptäcker att det finns ett betydande behov av detta baserat på feedback från kundanvändning kommer vi att undersöka en namngivningslösning som fungerar för funktionspekare, tupplar, generiska objekt etcetera. Detta kommer sannolikt att likna andra förslag i form, såsom fullständigt stöd för typedef i språket.

Framtida överväganden

statiska delegater

Detta avser förslaget att tillåta deklaration av delegate typer som endast får referera till static medlemmar. Fördelen är att sådana delegate instanser kan vara allokeringsfria och bättre i prestandakänsliga scenarier.

Om funktionen för funktionspekare implementeras kommer static delegate-förslaget sannolikt att avslutas. Den föreslagna fördelen med den funktionen är dess allokeringsfria natur. Nyligen genomförda undersökningar har dock visat att det inte är möjligt att uppnå på grund av monterings lossning. Det måste finnas ett starkt handtag från static delegate till den metod som den refererar till för att hålla sammansättningen från att lossas under den.

För att bevara varje static delegate-instans skulle det krävas att allokera ett nytt handtagsvärde, vilket strider mot de mål som föreslås i förslaget. Det fanns vissa designer där allokeringen kunde fördelas ut till en enda allokering per anropsställe, men det var lite komplext och verkade inte värt kompromissen.

Det innebär att utvecklare i princip måste välja mellan följande kompromisser:

  1. Säkerhet vid monteringsavlastning: detta kräver resurstilldelningar och därför är delegate redan ett tillräckligt bra alternativ.
  2. Ingen säkerhet vid monterings lossning: använd en delegate*. Detta kan omslutas i en struct för att tillåta användning utanför en unsafe kontext i resten av koden.