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):
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 enunsafe
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*
tillvoid*
. - Kan explicit konvertera från
void*
tilldelegate*
.
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 somparams
- 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 identifier
som ä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_type
F0
till en annan funcptr_typeF1
, förutsatt att allt följande är sant:-
F0
ochF1
har samma antal parametrar och varje parameterD0n
iF0
har sammaref
,out
ellerin
modifierare som motsvarande parameterD1n
iF1
. - För varje värdeparameter (en parameter utan
ref
,out
ellerin
modifierare) finns det en identitetskonvertering, implicit referenskonvertering eller implicit pekarkonvertering från parametertypen iF0
till motsvarande parametertyp iF1
. - För varje parameter
ref
,out
ellerin
är parametertypen iF0
samma som motsvarande parametertyp iF1
. - Om returtypen är ett värde (ingen
ref
ellerref readonly
) finns det en identitet, implicit referens eller implicit pekarkonvertering från returtypen avF1
till returtypen avF0
. - Om returtypen är per referens (
ref
ellerref readonly
) är returtypen ochref
modifierare förF1
desamma som returtypen ochref
modifierare förF0
. - Anropskonventionen för
F0
är densamma som den anropande konventionen förF1
.
-
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
ochF
har samma antal parametrar och varje parameter iM
har sammaref
,out
ellerin
modifierare som motsvarande parameter iF
. - För varje värdeparameter (en parameter utan
ref
,out
ellerin
modifierare) finns det en identitetskonvertering, implicit referenskonvertering eller implicit pekarkonvertering från parametertypen iM
till motsvarande parametertyp iF
. - För varje parameter
ref
,out
ellerin
är parametertypen iM
samma som motsvarande parametertyp iF
. - Om returtypen är efter värde (ingen
ref
ellerref readonly
) finns det en identitet, implicit referens eller implicit pekarkonvertering från returtypen förF
till returtypenM
. - Om returtypen anges som referens (
ref
ellerref readonly
) är returtypen ochref
-modifierarna avF
samma som returtypen ochref
-modifierarna avM
. - Anropskonventionen för
M
är densamma som den anropande konventionen förF
. 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äretE(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
,out
ellerin
) för motsvarande funcptr_parameter_list avF
. - 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.
- Argumentlistan
- 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 somF
och konverteringen anses finnas. - Den valda metoden
M
måste vara kompatibel (enligt definitionen ovan) med funktionspekartypenF
. 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:
- Operatorn
&
kan användas för att hämta adressen till statiska metoder (Tillåt adress för till målmetoder)- Operatorerna
==
,!=
,<
,>
,<=
och=>
kan användas för att jämföra pekare (§23.6.8).
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 änvoid*
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
Följande läggs till:
Om
E
är en metodgruppsadress ochT
är en funktionspekartyp är alla parametertyper avT
indatatyper avE
med typenT
.
Utdatatyper
Följande läggs till:
Om
E
är en metodgruppsadress ochT
är en typ av funktionspekare är returtypenT
en utdatatyp avE
med typenT
.
Slutsatsdragningar för utdatatyp
Följande punkt läggs till mellan punkterna 2 och 3:
- Om
E
är en metodgruppsadress ochT
är en funktionspekartyp med parametertyperT1...Tk
och returtypTb
och överlagringsmatchning avE
med typernaT1..Tk
ger en enda metod med returtypU
, görs en lägre bindningsslutsats frånU
tillTb
.
Bättre konvertering från uttryck
Följande underpunkt läggs till som ett ärende i punkt 2:
V
är en funktionspekartypdelegate*<V2..Vk, V1>
ochU
är en funktionspekartypdelegate*<U2..Uk, U1>
, och den anropande konventionen förV
är identisk medU
, och referensen förVi
är identisk medUi
.
Slutsatser med nedre gräns
Följande fall läggs till i punkt 3:
V
är en funktionspekartypdelegate*<V2..Vk, V1>
och det finns en funktionspekartypdelegate*<U2..Uk, U1>
så attU
är identisk meddelegate*<U2..Uk, U1>
, och anropskonventionen förV
är identisk medU
, och refnessen förVi
är identisk medUi
.
Den första inferensen från Ui
till Vi
ändras till:
- Om
U
inte är en funktionspekartyp ochUi
inte är känd för att vara en referenstyp, eller omU
är en funktionspekartyp ochUi
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
ärdelegate*<V2..Vk, V1>
beror annars slutsatsdragningen på parametern i-th idelegate*<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
Följande fall läggs till i punkt 2:
U
är en funktionspekartypdelegate*<U2..Uk, U1>
ochV
är en typ av funktionspekare som är identisk meddelegate*<V2..Vk, V1>
, och anropskonventionen förU
är identisk medV
, och referensen förUi
är identisk medVi
.
Den första inferensen från Ui
till Vi
ändras till:
- Om
U
inte är en funktionspekartyp ochUi
inte är känd för att vara en referenstyp, eller omU
är en funktionspekartyp ochUi
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
ärdelegate*<U2..Uk, U1>
beror annars slutsatsdragningen på parametern i-th idelegate*<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
, out
och ref readonly
parametrar och returtyper
Funktionspekarsignaturer har ingen plats för parameterflaggor, så vi måste koda om parametrar och returtypen är in
, out
eller 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
ochOutAttribute
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 modopt
i 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 modopt
s ä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 default
CallKind
. Detta är standard CallKind
för alla metoder som inte tillskrivs UnmanagedCallersOnly
.
C# identifierar 4 särskilda identifierare som mappar till specifika befintliga ohanterade CallKind
frå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_convention
s. Dessa identifierare är Cdecl
, Thiscall
, Stdcall
och Fastcall
, som motsvarar unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
respektive 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ängenCallConv
- 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 identifier
som anges i en unmanaged_calling_convention
kodar vi CallKind
som unmanaged ext
och kodar var och en av de lösta typerna i uppsättningen med modopt
s i början av funktionspekarsignaturen. Observera att dessa regler innebär att användarna inte kan prefix dessa identifier
s 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 ext
ignorerar vi alla modopt
på returtypen för att fastställa anropskonventionen och använder endast CallKind
. Om CallKind
är unmanaged ext
tittar 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å identifier
s 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 konventionenmodopt
s 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 modopt
som ska användas för att fastställa anropskonventionen:
- Om inga typer anges behandlas
CallKind
somunmanaged ext
, utan anropskonventionmodopt
s i början av funktionspekartypen. - Om det finns en angiven typ och den typen heter
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
ellerCallConvFastcall
behandlasCallKind
somunmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
respektiveunmanaged fastcall
, utan anropskonventionmodopt
s 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
somunmanaged ext
, med union av de angivna typerna som behandlas sommodopt
s 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 class
osv. ... 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å:
- Ha en unik typ för att tillåta överbelastningar med olika funktionspekartyper.
- 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:
- Säkerhet vid monteringsavlastning: detta kräver resurstilldelningar och därför är
delegate
redan ett tillräckligt bra alternativ. - Ingen säkerhet vid monterings lossning: använd en
delegate*
. Detta kan omslutas i enstruct
för att tillåta användning utanför enunsafe
kontext i resten av koden.
C# feature specifications