Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Jegyzet
Ez a cikk egy funkcióspecifikáció. A specifikáció a funkció tervezési dokumentumaként szolgál. Tartalmazza a specifikáció javasolt módosításait, valamint a funkció tervezése és fejlesztése során szükséges információkat. Ezeket a cikkeket mindaddig közzéteszik, amíg a javasolt specifikációmódosításokat nem véglegesítik, és be nem építik a jelenlegi ECMA-specifikációba.
A szolgáltatás specifikációja és a befejezett implementáció között eltérések lehetnek. Ezeket a különbségeket a vonatkozó nyelvi tervezési értekezlet (LDM) megjegyzései rögzítik.
A funkcióspektusok C# nyelvi szabványba való bevezetésének folyamatáról a specifikációkcímű cikkben olvashat bővebben.
Bajnoki problémák: https://github.com/dotnet/csharplang/issues/1147, https://github.com/dotnet/csharplang/issues/6476
Összefoglalás
Ez a javaslat számos különböző javaslat összesítése struct teljesítménybeli fejlesztésekre: ref mezőket és az élettartam alapértelmezett értékeinek felülbírálásának lehetőségét. A cél egy olyan terv, amely figyelembe veszi a különböző javaslatokat, hogy egy átfogó funkciókészletet hozzon létre, alacsony szintű struct fejlesztések számára.
Megjegyzés: A specifikáció korábbi verziói a "ref-safe-to-escape" és a "safe-to-escape" kifejezéseket használták, amelyek a Span biztonsági funkció specifikációjában lettek bevezetve. A ECMA szabványbizottsága a neveket "ref-safe-context", illetve "safe-context"névre módosította. A biztonságos környezet értékeit pontosítottuk annak érdekében, hogy következetesen használjuk a "Deklarálási Blokk", "Függvény-Tag" és "Hívó-Környezet" kifejezéseket. A specifikációk különböző kifejezéseket használtak ezekhez a kifejezésekhez, és a "safe-to-return" kifejezést is használták a "hívó-kontextus" szinonimájaként. Ez a specifikáció a C# 7.3 szabványban szereplő kifejezések használatára lett frissítve.
Nem minden, a dokumentumban ismertetett funkció implementálva lett a C# 11-ben. A C# 11 a következőket tartalmazza:
-
refmezők ésscoped [UnscopedRef]
Ezek a funkciók továbbra is nyitott javaslatok maradnak a C#jövőbeli verziójára:
-
refmezőkrőlref structmezőkre - Korlátozott naplementetípusok
Motiváció
A C# korábbi verziói számos alacsony szintű teljesítményfunkciót adtak hozzá a nyelvhez: ref visszaad, ref struct, függvénymutatók stb. ... Ezek lehetővé ták a .NET-fejlesztők számára, hogy nagy teljesítményű kódot írjanak, miközben továbbra is a C# nyelv szabályait használják a típus- és memóriabiztonság érdekében. Lehetővé tette az alapvető teljesítménytípusok létrehozását is a .NET-kódtárakban, például a Span<T>.
Mivel ezek a funkciók a .NET-ökoszisztéma fejlesztőinél mind belsőleg, mind külsőleg hasznosultak, információt szolgáltattak az ökoszisztéma fennmaradó súrlódási pontjairól. Azok a helyek, ahol továbbra is a unsafe kódot kell használniuk a munkájuk elvégzéséhez, vagy ahol a futtatókörnyezetnek speciálisan kell kezelnie bizonyos típusokat, például a Span<T>-et.
A(z) Span<T> folyamatot az internal típusú ByReference<T> használatával hajtják végre, amelyet a futtatókörnyezet hatékonyan ref mezőként kezel. Ez a ref mezők előnyeit biztosítja, de azzal a hátránysal, hogy a nyelv nem biztosít biztonsági ellenőrzést, ahogyan az refmás felhasználási módjai esetében is. Csak a dotnet/runtime használhatja ezt a típust, mivel ez internal, így a harmadik felek nem tudnak saját primitíveket tervezni ref mezők alapján. A munka
Ez a javaslat a meglévő alacsony szintű funkciókra építve tervezi kezelni ezeket a problémákat. Konkrétan a következő célokat célozza:
- Engedélyezze a
ref structtípusoknak, hogyrefmezőket deklaráljanak. - Engedélyezze a futtatókörnyezetnek, hogy teljes mértékben definiálja a
Span<T>a C#-típusrendszer használatával, és távolítsa el a speciális esettípusokat, példáulByReference<T> - Tegye lehetővé, hogy a
structtípusokrefadjanak vissza a mezőikhez. - Annak engedélyezése, hogy a futtatókörnyezet eltávolítsa a
unsafe-használatokat, amelyeket az élettartam alapértelmezett korlátai okoztak. - Biztonságos
fixedpufferek deklarációjának engedélyezése felügyelt és nem felügyelt típusok számárastruct
Részletes tervezés
A ref struct biztonságra vonatkozó szabályokat a biztonsági dokumentumban,, az előző kifejezések használatával határozzák meg. Azok a szabályok beépültek a C# 7 szabványba a §9.7.2 és §16.4.12részekben. Ez a dokumentum ismerteti a dokumentum szükséges módosításait a javaslat eredményeként. Miután elfogadta jóváhagyott funkcióként ezeket a módosításokat belefoglaljuk ebbe a dokumentumba.
A tervezés befejezése után a Span<T> definíció a következő lesz:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
// This constructor does not exist today but will be added as a part
// of changing Span<T> to have ref fields. It is a convenient, and
// safe, way to create a length one span over a stack value that today
// requires unsafe code.
public Span(ref T value)
{
_field = ref value;
_length = 1;
}
}
Hivatkozási mezők és hatókör megadása.
A nyelv lehetővé teszi, hogy a fejlesztők ref mezőket deklaráljanak egy ref structbelül. Ez, például, akkor lehet hasznos, ha nagyméretű módosítható struct példányokat foglal magába, vagy olyan, a kódtárakban a futtatási környezeten kívüli magas teljesítményű típusokat határoz meg, mint a Span<T>.
ref struct S
{
public ref int Value;
}
A ref aláírás használatával egy ELEMENT_TYPE_BYREF mezőt kibocsátanak a metaadatokba. Ez sem különbözik attól, ahogyan a ref helyi változókat vagy a ref argumentumokat generáljuk. Például a ref int _field-ként fog megjelenni, mint ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4. Ehhez frissíteni kell az ECMA335-öt, hogy engedélyezhessük ezt a bejegyzést, de ennek egyszerű kell, hogy legyen.
A fejlesztők továbbra is inicializálhatnak egy ref struct egy ref mezővel a default kifejezés használatával, amely esetben az összes deklarált ref mező értéke nulllesz. Az ilyen mezők használatára tett kísérletek NullReferenceException eredményeznek.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Bár a C#-nyelv azt szimulálja, hogy egy ref nem lehet null, ez futtatási szinten legális, és jól definiált szemantikával rendelkezik. Azoknak a fejlesztőknek, akik ref mezőket vezetnek be a típusukba, tisztában kell lenniük ezzel a lehetőséggel, és határozottan el kell kerülniük, hogy ezt a részletet kiszivárogtathassák a kód felhasználásakor. Ehelyett a ref mezőket null értéken kívülinek kell ellenőrizni a futásidejű segítők segítségével, és kivételt dobva, ha egy nem inicializált struct-at helytelenül használnak.
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
A ref mezők az alábbi módokon kombinálhatók readonly módosítókkal:
-
readonly ref: olyan mező, amely konstruktoron vagyinitmetóduson kívül nem rendelhető át újra. Érték rendelhető hozzá ezen kívüli környezetekben is. -
ref readonly: ez egy olyan mező, amely újból hozzárendelhető, de nem rendelhető hozzá semmilyen érték. Így lehet egyinparamétert újra hozzárendelni egyrefmezőhöz. -
readonly ref readonly:ref readonlyésreadonly refkombinációja.
ref struct ReadOnlyExample
{
ref readonly int Field1;
readonly ref int Field2;
readonly ref readonly int Field3;
void Uses(int[] array)
{
Field1 = ref array[0]; // Okay
Field1 = array[0]; // Error: can't assign ref readonly value (value is readonly)
Field2 = ref array[0]; // Error: can't repoint readonly ref
Field2 = array[0]; // Okay
Field3 = ref array[0]; // Error: can't repoint readonly ref
Field3 = array[0]; // Error: can't assign ref readonly value (value is readonly)
}
}
A readonly ref struct esetében szükséges lesz, hogy a ref mezők readonly refmódon legyenek deklarálva. Nincs szükség arra , hogy readonly ref readonlydeklarálva legyenek. Ez lehetővé teszi, hogy egy readonly struct közvetett mutációval rendelkezzen egy ilyen mezőn keresztül, de ez nem különbözik egy olyan readonly mezőnél, amely a mai referenciatípusra mutatott (további részletek)
A readonly ref a initonly jelzővel a többi mezőhöz hasonlóan kerül kibocsátásra a metaadatokba. A ref readonly mezőhöz hozzá lesz rendelve System.Runtime.CompilerServices.IsReadOnlyAttribute. Mindkét elemmel együtt ki lesz bocsátva egy readonly ref readonly.
Ehhez a funkcióhoz futásidejű támogatásra és az ECMA-specifikáció módosítására van szükség. Ezért ezek csak akkor lesznek engedélyezve, ha a megfelelő funkciójelző be van állítva a Corelib-ben. Az API pontos nyomon követését érintő probléma itt található: https://github.com/dotnet/runtime/issues/64165
A ref mezők engedélyezéséhez szükséges biztonságos környezeti szabályok módosítása kis méretű és célzott. A szabályok már figyelembe veszik a ref mezőket, amelyek léteznek és API-kból kerülnek felhasználásra. A módosításoknak csak két szempontra kell összpontosítaniuk: a létrehozásuk módjára és az újbóli hozzárendelésre.
Először a szabályokat, amelyek a ref-safe-context értékeket állapítják meg a ref mezők számára, az alábbiak szerint kell frissíteni.
Egy kifejezés
ref e.Fref-safe-context formájában az alábbiak szerint:
- Ha
Fegyrefmező, akkor a ref-safe-context a biztonságos környezet aeszámára.- Ha máskülönben
referenciatípusú, akkor ref-biztonságos-környezetként van jelen a .hívókörnyezeti - Különben a ref-safe-context a
e-ből van átvéve.
Ez nem jelent szabálymódosítást, mivel a szabályok mindig is figyelembe veszik, hogy ref állapot létezik egy ref structbelül. Valójában a ref állapot a Span<T>-ben mindig is így működött, és a fogyasztási szabályok ezt helyesen veszik figyelembe. A változás annak érdekében történik, hogy a fejlesztők közvetlenül hozzáférhessenek a ref mezőkhöz, és biztosítva legyen, hogy a meglévő szabályokat, amelyek implicit módon érvényesek a Span<T>-re, betartsák.
Ez azt jelenti azonban, hogy a ref mezők visszaadhatók ref-ként egy ref struct-ből, de a normál mezők nem.
ref struct RS
{
ref int _refField;
int _field;
// Okay: this falls into bullet one above.
public ref int Prop1 => ref _refField;
// Error: This is bullet four above and the ref-safe-context of `this`
// in a `struct` is function-member.
public ref int Prop2 => ref _field;
}
Ez első pillantásra hiba lehet, de ez szándékos tervezési pont. Ez azonban nem egy új szabály, amelyet ez a javaslat hoz létre, hanem elismeri a meglévő szabályokat, Span<T> úgy viselkednek, hogy a fejlesztők saját ref állapotukat deklarálhatják.
Ezután módosítani kell a referencia-újraosztási szabályokat a ref mezők jelenlétéhez. Az újbóli hozzárendelés elsődleges forgatókönyve ref struct konstruktorok, amelyek ref paramétereket tárolnak ref mezőkbe. A támogatás általánosabb lesz, de ez az alapvető forgatókönyv. Ennek támogatásához a hivatkozás visszajelölési szabályait módosítják úgy, hogy figyelembe vegyék a ref mezőket, az alábbiak szerint:
Referencia-hozzárendelési szabályok
A = ref operátor bal operandusának olyan kifejezésnek kell lennie, amely egy ref helyi változóhoz, egy ref paraméterhez (a thiskivételével), egy kimenő paraméterhez, egy vagy egy ref mezőhözkapcsolódik.
Az
e1 = ref e2formátumú áthivatkozás újrahozzárendelése esetén mindkét feltételnek igaznak kell lennie:
e2ref-safe-context legalább akkora kell legyen, minte1.e1ugyanolyan biztonságos környezetben kell lennie, minte2Megjegyzés
Ez azt jelenti, hogy a kívánt Span<T> konstruktor további megjegyzések nélkül működik:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
public Span(ref T value)
{
// Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The
// safe-context of `this` is *return-only* and ref-safe-context of `value` is
// *caller-context* hence this is legal.
_field = ref value;
_length = 1;
}
}
A ref újra-hozzárendelési szabályok módosítása azt jelenti, hogy a ref paraméterek most már egy metódusból való kilépéskor ref mezőként jelenhetnek meg egy ref struct értékben. A kompatibilitási szempontok című szakaszban tárgyaltak szerint ez megváltoztathatja az olyan meglévő API-k szabályait, amelyeknél a ref paraméterek soha nem voltak szánva arra, hogy ref mezőként kerüljenek ki. A paraméterek élettartamára vonatkozó szabályok kizárólag a deklaráción alapulnak, nem pedig a használatukon. Minden ref és in paraméter hívókörnyezeti (ref-safe-context ), ezáltal most már ref vagy egy ref mező által visszaadhatók. Annak érdekében, hogy az API-k ref olyan paraméterekkel rendelkezhessenek, amelyek menekülhetnek vagy nem menekülhetnek, és így visszaállíthatják a C# 10 híváswebhely szemantikáját, a nyelv korlátozott élettartamú széljegyzeteket vezet be.
scoped módosító
A scoped kulcsszó egy érték élettartamának korlátozására szolgál. Alkalmazható egy ref értékre vagy egy olyan értékre, amely ref struct, és hatással van a ref-safe-context, illetve a biztonságos környezet élettartamának korlátozására a függvénytag-ig. Például:
| Paraméter vagy helyi érték | ref-safe-context | biztonságos környezet |
|---|---|---|
Span<int> s |
függvénytag | hívókörnyezet |
scoped Span<int> s |
függvénytag | függvénytag |
ref Span<int> s |
hívókörnyezet | hívókörnyezet |
scoped ref Span<int> s |
függvénytag | hívókörnyezet |
Ebben a kapcsolatban az érték ref-safe-context soha nem lehet szélesebb, mint a biztonságos környezet.
Ez lehetővé teszi, hogy a C# 11 API-k úgy legyenek jegyzetelve, hogy ugyanazok a szabályok legyenek, mint a C# 10:
Span<int> CreateSpan(scoped ref int parameter)
{
// Just as with C# 10, the implementation of this method isn't relevant to callers.
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 and legal in C# 11 due to scoped ref
return CreateSpan(ref parameter);
// Legal in C# 10 and legal in C# 11 due to scoped ref
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 and legal in C# 11 due to scoped ref
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
A scoped megjegyzés azt is jelenti, hogy egy thisstruct paramétere mostantól scoped ref Tként definiálható. Korábban speciális esetnek kellett lennie a szabályokban, mint ref paraméter, amely más ref-safe-context szabályokat tartalmazott, mint más ref paraméterek (lásd a fogadó biztonságos környezeti szabályokba való belefoglalására vagy kizárására vonatkozó összes hivatkozást). Most már általános fogalomként is kifejezhető a szabályokban, ami tovább egyszerűsíti őket.
A scoped széljegyzet a következő helyekre is alkalmazható:
- locals: Ez a megjegyzés a biztonságos környezetvagy esetén egy helyi, a
reffüggvénytag függetlenül az inicializáló élettartamától.
Span<int> ScopedLocalExamples()
{
// Error: `span` has a safe-context of *function-member*. That is true even though the
// initializer has a safe-context of *caller-context*. The annotation overrides the
// initializer
scoped Span<int> span = default;
return span;
// Okay: the initializer has safe-context of *caller-context* hence so does `span2`
// and the return is legal.
Span<int> span2 = default;
return span2;
// The declarations of `span3` and `span4` are functionally identical because the
// initializer has a safe-context of *function-member* meaning the `scoped` annotation
// is effectively implied on `span3`
Span<int> span3 = stackalloc int[42];
scoped Span<int> span4 = stackalloc int[42];
}
A scoped egyéb helyi alkalmazási módjait az alábbiakban tárgyaljuk .
A scoped annotáció nem alkalmazható más helyszínekre, beleértve a visszatérési értékeket, mezőket, tömbelemeket stb. Továbbá, míg a scoped bármilyen ref-re, in-ra vagy out-re alkalmazva hatással bír, csak a ref structjellemzőkkel rendelkező értékekre alkalmazva van hatással. A scoped int-hez hasonló deklarációknak nincs hatása, mert mindig biztonságosan visszatérhetünk, ha nincs ref struct. A fordító létrehoz egy diagnosztikát az ilyen esetekhez a fejlesztői félreértések elkerülése érdekében.
out paraméterek viselkedésének módosítása
A ref és in paraméterek ref mezőként való visszaadásának kompatibilis változásának további korlátozása érdekében a nyelv megváltoztatja az alapértelmezett ref-safe-context értéket a out paraméterekhez, hogy függvénytaglegyen. Gyakorlatilag out paraméterek implicit módon scoped out fognak előrehaladni. A compat szemszögéből ez azt jelenti, hogy refnem tudja visszaadni őket:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
Ez növeli azon API-k rugalmasságát, amelyek ref struct értékeket adnak vissza, és out paraméterekkel rendelkeznek, mivel a paramétert már nem kell hivatkozással rögzíteni. Ez azért fontos, mert ez egy gyakori minta az olvasóstílus API-kban:
Span<byte> Read(Span<byte> buffer, out int read)
{
// ..
}
Span<byte> Use()
{
var buffer = new byte[256];
// If we keep current `out` ref-safe-context this is an error. The language must consider
// the `read` parameter as returnable as a `ref` field
//
// If we change `out` ref-safe-context this is legal. The language does not consider the
// `read` parameter to be returnable hence this is safe
int read;
return Read(buffer, out read);
}
A nyelv a továbbiakban nem fogja visszatéríthetőnek tekinteni az out paraméternek átadott argumentumokat. A out paraméter bemenetének visszaadhatóként való kezelése rendkívül zavaró volt a fejlesztők számára. Ez lényegében aláássa a out szándékát azáltal, hogy arra kényszeríti a fejlesztőket, hogy vegyék figyelembe a hívó által átadott értéket, amelyet soha nem használnak, kivéve azokat a nyelveket, amelyek nem tartják be a outelvet. A ref struct támogató nyelveknek gondoskodniuk kell arról, hogy a out paraméternek átadott eredeti érték soha ne legyen olvasható.
A C# ezt meghatározott hozzárendelési szabályokkal éri el. Mindkettő biztosítja a ref biztonságos környezeti szabályait, valamint lehetővé teszi a meglévő kód használatát, amely hozzárendeli, majd visszaadja out paraméterértékeket.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Ezek a módosítások együttesen azt jelentik, hogy egy out paraméter argumentuma nem járul hozzá biztonságos környezet vagy újraf-safe-context értékekhez a metódushívásokhoz. Ez jelentősen csökkenti a ref mezők általános kompatitiv hatását, valamint leegyszerűsíti a fejlesztők gondolkodását out. Egy out paraméter argumentuma nem járul hozzá a visszatéréshez, ez egyszerűen egy kimenet.
Deklarációs kifejezések biztonságos környezetének következtetésére
A deklarációs változó biztonságos környezete az out argumentumból (M(x, out var y)) vagy dekonstruálásból ((var x, var y) = M()) származó legszűkebb:
- hívókörnyezet
- ha a kimenő változó
scopedmeg van jelölve, ez azt jelenti, hogy a deklarációs blokk (például függvényhez tartozó elem vagy ennél keskenyebb). - ha a kimenő változó típusa
ref struct, vegye figyelembe a meghívást tartalmazó argumentumokat, beleértve a fogadót is:-
olyan argumentum esetében, amelynél a hozzá tartozó paraméter nem
out, és amely csak visszatérési vagy ennél szélesebb biztonságos környezettel rendelkezik - ref-safe-context bármely argumentumnál, ahol a megfelelő paraméter ref-safe-contextcsak visszatérési vagy szélesebb
-
olyan argumentum esetében, amelynél a hozzá tartozó paraméter nem
Lásd még A deklarációs kifejezésekpéldáit a következtetett biztonságos környezettel.
Implicit módon scoped paraméterek
Összesen két ref hely van, amelyek implicit módon scopeddeklarálva vannak:
-
thisegystruct-példány metódusán -
outparaméterek
A ref safe környezeti szabályok a scoped ref és refalapján lesznek megírva. A hivatkozásbiztos környezet érdekében a in paraméter egyenértékű a ref-gyel, és a out egyenértékű a scoped ref-mal. Mind a in, mind a out csak akkor lesz külön kiemelve, ha fontos a szabály jelentése szempontjából. Ellenkező esetben csak ref és scoped ref tekintjük őket.
A paramétereknek megfelelő argumentumok in tárgyalásakor azok a specifikációban ref argumentumként lesznek általánosítva. Abban az esetben, ha az argumentum lvalue, akkor a ref-safe-context az lvalue értéke, ellenkező esetben függvénytag. Ismét in csak akkor hívjuk meg itt, ha fontos az aktuális szabály szemantikai szempontja.
Csak visszatérési biztonságos környezet
A kialakítás megköveteli egy új, kizárólag visszatérésre szolgáló biztonságos környezet bevezetését is: . Ez hasonló
A visszatérésre vonatkozó részletei az, hogy ez egy olyan környezet, amely nagyobb, mint a függvénytag, de kisebb, mint a hívói környezet. A return utasításhoz megadott kifejezésnek legalább visszatérési célúkell lennie. Mivel a legtöbb meglévő szabály érvénytelennek minősül. Ha például egy ref paraméterhez egy kifejezés biztonságos környezetével csak visszatérési, az azért fog meghiúsulni, mert kisebb, mint az ref paraméter biztonságos környezete, amely hívókörnyezeti. Az új menekülési környezet szükségességét az alábbi tárgyaljuk.
Három hely van, amelyek alapértelmezés szerint csak visszatérési
- Egy
refvagyinparaméter ref-safe-context csak visszatérési . Ez részben azért történikref struct, hogy megakadályozza a buta ciklikus hozzárendeléssel kapcsolatos problémákat. Ez azonban egységesen történik a modell egyszerűsítése és a kompatibilitási változások minimalizálása érdekében. - Egy
outparaméterref structesetén biztonságos kontextus-ként fog megjelenni, ami csak a visszatérésről szól. Ez lehetővé teszi, hogy a visszatérés és aoutegyaránt kifejezőek legyenek. Ez nem szenved a buta ciklikus hozzárendelési problémától, mert aoutimplicit módonscoped, így a ref-safe-context még mindig kisebb, mint a biztonságos környezet. - Egy
konstruktor paramétere . Ezcsak visszatérési outparaméterként való modellezés miatt kiesik.
Bármely kifejezésnek vagy utasításnak, amely egy metódusból vagy lambdából explicit módon ad vissza egy értéket, biztonságos környezettelkell rendelkeznie, és ha alkalmazható, legalább kizárólag visszatérésiref-safe-context. Ez magában foglalja a return állításokat, a kifejezési törzsű tagokat és a lambda kifejezéseket.
Hasonlóképpen egy out hozzárendelésnek legalább csak visszatérésibiztonságos környezettel kell rendelkeznie. Ez azonban nem különleges eset, csak a meglévő hozzárendelési szabályokból következik.
Megjegyzés: A nem ref struct típusú kifejezések mindig biztonságos-környezetihívókörnyezetirendelkeznek.
Metódushívás szabályai
A metódushívásokra vonatkozó ref biztonságos környezeti szabályok több módon is frissülnek. Az első a scoped argumentumokra gyakorolt hatásának felismerése. Egy adott argumentum expr, amelyet a pparaméternek adunk át:
- Ha
pscoped ref, akkorexprnem járul hozzá ref-safe-context az argumentumok mérlegeléséhez.- Ha
pscoped, akkorexprnem járul hozzá biztonságos környezethez az argumentumok mérlegelésekor.- Ha
pout, akkorexprnem járul hozzá ref-safe-context vagy biztonságos környezettovábbi részletek
A "nem járul hozzá" nyelv azt jelenti, hogy az argumentumokat egyszerűen nem veszik figyelembe a ref-safe-context vagy biztonságos környezet metódus visszatérési értékének kiszámításakor. Ennek az az oka, hogy az értékek nem járulhatnak hozzá az adott élettartamhoz, mivel a scoped széljegyzet megakadályozza.
A metódushívási szabályok mostantól egyszerűsíthetők. A fogadónak már nem kell külön elbírálás, abban az esetben, ha struct, ez most egyszerűen scoped ref T. Az értékszabályokat módosítani kell, hogy figyelembe vegyék ref mező visszatérési értékét:
A metódushívás
e1.M(e2, ...)eredményeként kapott érték, aholM()nem ad vissza ref-to-ref-struct értéket, a következőkből a legszűkebbet:
- A hívókörnyezet
- Ha a visszatérés egy
ref structa biztonságos környezet, amelyet az összes argumentumkifejezés tartalmaz- Ha a visszatérés egy
ref struct, akkor a ref-safe-context mindenrefargumentum hozzájárulása.Ha a
M()ref-to-ref-struct típust ad vissza, akkor a biztonságos környezet ugyanaz, mint a ref-to-ref-struct típusú összes argumentum . Hiba, ha több argumentum különböző biztonságos környezetben van, mivel a metódusargumentumoknak meg kell egyezniük.
A ref hívási szabályai a következőre egyszerűsíthetők:
A metódushívás
ref e1.M(e2, ...)eredményeként kapott érték – aholM()nem ad vissza ref-to-ref-struct értéket – a ref-safe-context a következő környezetek közül a legszűkebb:
- A hívókörnyezet
- Az összes argumentumkifejezés hozzájárulásával létrejött biztonságos környezet
- Az ref-safe-context az összes
refargumentum hozzájárultHa a
M()ref-to-ref-struct értéket ad vissza, akkor a ref-safe-context a legszűkebb ref-safe-context, amelyhez az összes ref-to-ref-struct argumentum hozzájárul.
Ez a szabály most lehetővé teszi a kívánt metódusok két változatának meghatározását:
Span<int> CreateWithoutCapture(scoped ref int value)
{
// Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *function-member* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> CreateAndCapture(ref int value)
{
// Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *caller-context* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
// Okay: the safe-context of `span` is *caller-context* hence this is legal.
return span;
// Okay: the local `refLocal` has a ref-safe-context of *function-member* and a
// safe-context of *caller-context*. In the call below it is passed to a
// parameter that is `scoped ref` which means it does not contribute
// ref-safe-context. It only contributes its safe-context hence the returned
// rvalue ends up as safe-context of *caller-context*
Span<int> local = default;
ref Span<int> refLocal = ref local;
return ComplexScopedRefExample(ref refLocal);
// Error: similar analysis as above but the safe-context of `stackLocal` is
// *function-member* hence this is illegal
Span<int> stackLocal = stackalloc int[42];
return ComplexScopedRefExample(ref stackLocal);
}
Az objektum inicializálóinak szabályai
Az objektum inicializáló kifejezésének biztonságos környezet a legszűkebb:
- A konstruktorhívás biztonságos környezet.
- Az argumentumok biztonságos környezete és ref-biztonságos-környezete a tag inicializáló indexelőihez, amelyek elérhetnek a fogadóhoz.
- A tag-inicializálókban lévő hozzárendelések RHS-ének biztonságos környezet nem olvasható beállítókhoz vagy ref-safe-context ref-hozzárendelés esetén.
A modellezés másik módja, ha egy tag inicializálójának bármilyen argumentumát a fogadóhoz rendelheti, mint a konstruktor argumentumát. Ennek az az oka, hogy a tag inicializálója tulajdonképpen konstruktorhívás.
Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
Field = stackSpan;
}
// Can be modeled as
var x = new S(ref heapSpan, stackSpan);
Ez a modellezés azért fontos, mert azt mutatja, hogy a MAMM kifejezetten figyelembe kell vennie a tagok inicializálóit. Vegye figyelembe, hogy ennek a konkrét esetnek illegálisnak kell lennie, mivel lehetővé teszi egy szűkebb biztonságos környezettel rendelkező egy magasabb értékhez való hozzárendelését.
A metódusargumentumoknak egyeznie kell
A ref mezők jelenléte azt jelenti, hogy a metódusargumentumok körüli szabályokat frissíteni kell, mivel egy ref paraméter mostantól mezőként tárolható a metódus ref struct argumentumában. Korábban a szabálynak figyelembe kellett vennie egy másik ref struct mezőként való tárolását. Tárgyaljuk ennek hatását a kompatibilitási megfontolások. Az új szabály...
Bármilyen metódushíváshoz
e.M(a1, a2, ... aN)
- Számítsa ki a legszűkebb biztonsági kontextus alapján:
- hívókörnyezet
- Minden argumentum biztonságos kontextusa
- Azon ref-argumentumok ref-safe-context, amelyek megfelelő paraméterei ref-safe-contexthívókörnyezeti
- Minden
refargumentumnak, amelyref structtípusú, hozzárendelhetőnek kell lennie olyan érték által, amely biztonságos környezetben található. Ez az az eset, amikorrefnem általánosítani, hogyinésout
Bármilyen metódushíváshoz
e.M(a1, a2, ... aN)
- Számítsa ki a legszűkebb biztonsági kontextus alapján:
- hívókörnyezet
- Minden argumentum biztonságos kontextusa
- A ref-safe-context minden ref argumentum esetében, amelyek megfelelő paraméterei nem
scoped- Minden
outargumentumnak, amelyref structtípusú, hozzárendelhetőnek kell lennie olyan érték által, amely biztonságos környezetben található.
A scoped jelenléte lehetővé teszi a fejlesztők számára, hogy csökkentsék a szabály által létrehozott súrlódást a nem scopedvisszaadott paraméterek megjelölésével. Ez mindkét fenti esetben eltávolítja az (1) argumentumokat, és nagyobb rugalmasságot biztosít a hívók számára.
Ennek a változásnak a hatását részletesebben alatt tárgyaljuk . Összességében ez lehetővé teszi a fejlesztők számára, hogy rugalmasabbá tegyék a hívási helyeket azáltal, hogy a nem menekülő ref-szerű értékeket a scopedannotációval látják el.
Paraméter hatókörének varianciája
A paraméterekre vonatkozó scoped módosító és [UnscopedRef] attribútum (lásd ) az objektum felülírására, a felület implementálására és delegate konverziós szabályokra is hatással van. A felülbírálás, a felület implementációjának vagy a delegate konverzió aláírása lehet a következő:
-
scopedhozzáadásarefvagyinparaméterhez -
scopedhozzáadásaref structparaméterhez -
[UnscopedRef]eltávolítása egyoutparaméterből -
[UnscopedRef]eltávolítása egyreftípusúref structparaméterből
Bármilyen más különbség a scoped vagy [UnscopedRef] tekintetében eltérésnek számít.
A fordító diagnosztikai jelentést fog készíteni a nem biztonságos hatókörű eltérések felülbírálások, interfész-implementációk és delegált konverziók közötti inkonzisztenciáiról, ha:
- A metódus
reftípusúoutvagyref structparaméterrel rendelkezik, amely nem egyezik a[UnscopedRef]hozzáadásával (nem távolítja elscoped). (Ebben az esetben egy buta ciklikus hozzárendelés lehetséges, ezért nincs szükség más paraméterekre.) - Vagy mindkettő igaz:
- A metódus egy
ref struct-t ad vissza, vagy egyref-t vagyref readonly-t ad vissza, vagy a metódus rendelkezik egyrefvagyoutparaméterrel, amelyref structtípusú. - A metódus legalább egy további
ref,invagyoutparaméterrel vagyref structtípusú paraméterrel rendelkezik.
- A metódus egy
A diagnosztikát más esetekben nem jelentik, mert:
- Az ilyen aláírásokkal rendelkező metódusok nem tudják rögzíteni a továbbított referenciákat, így semmilyen hatókör-eltérés nem veszélyes.
- Ezek közé tartoznak a nagyon gyakori és egyszerű forgatókönyvek (például az egyszerű régi
outparaméterek, amelyeketTryParsemetódus-aláírásokban használnak), és a hatókörön belüli eltérések jelentése csak azért, mert a 11-es verzióban használják őket (és ezért aoutparaméter hatóköre eltérő) zavaró lenne.
A diagnosztika hibaként jelenik meg, ha a nem egyező aláírások mindkét esetben a C#11 "ref" biztonságos környezeti szabályokat használják. Ellenkező esetben a diagnosztika figyelmeztetésként jelenik meg.
A hatókörön belüli eltérésre vonatkozó figyelmeztetés egy C#7.2-ref biztonságos környezetszabályokkal összeállított modulon jelenthető meg, ahol scoped nem érhető el. Bizonyos esetekben szükség lehet a figyelmeztetés mellőzésére, ha a másik nem egyező aláírás nem módosítható.
A scoped módosító és [UnscopedRef] attribútum a következő hatással van a metódus-aláírásokra:
- A
scopedmódosító és[UnscopedRef]attribútum nem befolyásolja a elrejtést - A túlterhelések nem különbözhetnek csak
scopedvagy[UnscopedRef]alapján.
A ref mező és scoped fejezet hosszú, ezért a javasolt jelentős változások rövid összefoglalójával szeretném zárni:
- A ref-safe-context értékkel rendelkező érték, amely a hívókörnyezeti viszonyban áll, visszaadható a
refvagyrefmező által. - Egy
outparaméter függvénytag.
Részletes megjegyzések:
- Egy
refmező csak egyref structbelül deklarálható - Egy
refmező nem deklarálhatóstatic,volatilevagyconst - Egy
refmező nem rendelkezhet olyan típussal, amelyref struct - A referenciaszerelvény-létrehozási folyamatnak meg kell őriznie egy
refmező jelenlétét egyref struct-ben. - A
readonly ref struct-nak deklarálnia kell arefmezőitreadonly ref-ként - A hivatkozott értékek esetében a
scopedmódosítónakin,outvagyrefelőtt kell megjelennie. - A biztonsági szabályokra vonatkozó dokumentum a jelen dokumentumban ismertetett módon frissül
- Az új ref safe környezeti szabályok akkor lesznek érvényben, ha
- Az alapvető kódtár tartalmazza az
refmezők támogatását jelző funkciójelzőt - A
langversionértéke 11 vagy magasabb
- Az alapvető kódtár tartalmazza az
Szintaxis
13.6.2 Helyi változódeklarációk: hozzáadva 'scoped'?.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 A for kijelentést hozzáadták: 'scoped'?közvetettenlocal_variable_declaration.
13.9.5 A foreach utasítás: hozzáadva 'scoped'?.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Argumentumlista: 'scoped'? deklarációs változóhoz hozzáadott out.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
12.7 dekonstruálási kifejezések:
[TBD]
15.6.2 metódus paraméterei: hozzáadva 'scoped'? a parameter_modifier.
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
parameter_modifier
| 'this' 'scoped'? parameter_mode_modifier?
| 'scoped' parameter_mode_modifier?
| parameter_mode_modifier
;
parameter_mode_modifier
: 'in'
| 'ref'
| 'out'
;
20.2 Delegált deklarációk: 'scoped'?közvetett módon hozzáadvafixed_parameter.
12.19 Névtelen függvénykifejezések: hozzáadva 'scoped'?.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Korlátozott naplementetípusok
A fordítónak van egy fogalma a "korlátozott típusok" készletéről, amely nagyrészt dokumentálatlan. Ezek a típusok különleges állapotot kaptak, mert a C# 1.0-ban nem volt általános célú módszer a viselkedésük kifejezésére. Különösen az a tény, hogy a típusok tartalmazhatnak a végrehajtási veremre mutató hivatkozásokat. Ehelyett a fordító speciális ismeretekkel rendelkezett róluk, és olyan módokra korlátozta a használatát, amelyek mindig biztonságosak lennének: letiltott visszatérések, nem használhatók tömbelemekként, nem használhatók általánosakban stb. ...
Ha ref mezők elérhetővé válnak és ki vannak terjesztve, hogy támogassák ref struct ezek a típusok helyesen definiálhatók c# nyelven ref struct és ref mezők kombinációjával. Ezért ha a fordító észleli, hogy egy futtatókörnyezet támogatja ref mezőket, akkor a továbbiakban nem fog korlátozott típusokat használni. Ehelyett a kódban definiált típusokat fogja használni.
Ennek támogatása érdekében a ref biztonságos környezet szabályai az alábbiak szerint frissülnek:
-
__makerefmetódusként lesz kezelve az aláírássalstatic TypedReference __makeref<T>(ref T value) -
__refvalueaz aláírásistatic ref T __refvalue<T>(TypedReference tr)metódusként fogjuk kezelni. A__refvalue(tr, int)kifejezés a második argumentumot fogja használni típusparaméterként. -
__arglistparaméterként rendelkezik a ref-biztonságos környezet-vel és a biztonságos környezet-vel a függvénytag-ból. - A
__arglist(...)kifejezéshez ref-biztos-környezet és biztos-környezet kapcsolódik a függvénytag.
A megfelelő futtatási környezetek biztosítják, hogy TypedReference, RuntimeArgumentHandle és ArgIteratorref structszerint definiálva legyenek. További TypedReference-t úgy kell tekinteni, mint aminek van egy ref mezője, a ref struct pedig bármely lehetséges típus számára, mert bármilyen értéket tárolhat. Ez a fenti szabályokkal kombinálva biztosítja, hogy a veremre mutató hivatkozások ne meneküljenek az élettartamukon túl.
Megjegyzés: Szigorúan véve ez egy fordító implementációs részlete, nem pedig a nyelv egy része. A ref mezőkkel való kapcsolatot figyelembe véve azonban az egyszerűség kedvéért szerepel a nyelvi javaslatban.
Hatókör nélkül biztosítani
Az egyik legfontosabb súrlódási pont az, hogy nem lehet mezőket visszaadni ref például egy structtagjai számára. Ez azt jelenti, hogy a fejlesztők nem hozhatnak létre ref visszatérési értékű metódusokat vagy tulajdonságokat, hanem közvetlen módon kell megjeleníteniük a mezőket. Ez csökkenti a ref hasznosságát struct-ben, ahol gyakran a legkívánatosabb.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
Az alapértelmezett észszerűnek tekinthető, de nincs semmi baj azzal, ha a struct elől menekülünk a this használatával; ez egyszerűen a ref safe környezeti szabályok által választott alapértelmezés.
Ennek kijavításához a nyelv az scoped élettartam-megjegyzés ellentétét úgy biztosítja, hogy támogatja a UnscopedRefAttribute-et. Ez bármely ref-ra alkalmazható, és a ref-safe-context-t egy szinttel szélesebbre állítja be az alapértelmezettnél. Például:
| UnscopedRef alkalmazva valamire | Eredeti ref-safe-context | Új ref-safe-context |
|---|---|---|
| példánytag | függvény-tag | csak visszaküldhető |
in
/
ref paraméter |
csak visszaküldhető | hívókörnyezet |
out paraméter |
függvény-tag | csak visszaküldhető |
Amikor a(z) [UnscopedRef]-t alkalmazzák egy struct példánymetóduson, az hatással van az implicit this paraméterre. Ez azt jelenti, hogy this azonos típusú, névtelen ref ként működik.
struct S
{
int field;
// Error: `field` has the ref-safe-context of `this` which is *function-member* because
// it is a `scoped ref`
ref int Prop1 => ref field;
// Okay: `field` has the ref-safe-context of `this` which is *caller-context* because
// it is a `ref`
[UnscopedRef] ref int Prop1 => ref field;
}
A jelölés elhelyezhető a out paraméterekre, hogy visszaállítsa a C# 10 viselkedésmódját.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
A hivatkozás biztonságos környezetének szabályai szerint az ilyen [UnscopedRef] out az egyszerűen ref-nek minősül. Hasonlóan ahhoz, hogy a in élettartam szempontjából ref-nek tekinthető.
A [UnscopedRef] megjegyzés nem lesz engedélyezett a init tagokon és konstruktorokon belül a struct. Ezek a tagok már különlegesek a ref szemantikájával kapcsolatban, mivel a readonly tagokat változtathatónak tekintik. Ez azt jelenti, hogy a ref eljuttatása ezekhez a tagokhoz egyszerű ref-ként jelenik meg, nem pedig ref readonly-ként. Ez a konstruktorok és inithatárain belül engedélyezett. A [UnscopedRef] engedélyezése lehetővé tenné, hogy egy ilyen ref helytelenül kilépjen a konstruktoron kívül, és lehetővé tenné a mutációt readonly szemantikák lezajlása után.
Az attribútumtípus definíciója a következő:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Részletes megjegyzések:
- A
jegyzettel ellátott példánymetódus vagy tulajdonság ref-safe-context.hívókörnyezeti - A
[UnscopedRef]megjelöléssel ellátott tag nem képes interfészt implementálni. - Hiba a
[UnscopedRef]használata- Olyan tag, aki nincs deklarálva egy
structelemre - Egy
statictag, egyinittag vagy egy konstruktor egystruct - A
scoped-val megjelölt paraméter - Érték szerint átadott paraméter
- Hivatkozással átadott paraméter, amely nem implicit hatókörű
- Olyan tag, aki nincs deklarálva egy
ScopedRefAttribute
A scoped széljegyzetek metaadatokká lesznek bocsátva a System.Runtime.CompilerServices.ScopedRefAttribute attribútum típusán keresztül. Az attribútum a névtérrel minősített név alapján lesz megfeleltetve, így a definíciónak nem kell megjelennie egyetlen adott összeállításban sem.
A ScopedRefAttribute típus csak fordítóhoz használható – a forrásban nem engedélyezett. A típusdeklarációt a fordító szintetizálja, ha még nem szerepel a fordításban.
A típus definíciója a következő lesz:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
A fordító hozzárendeli ezt az attribútumot a paraméterhez a scoped szintaxisával. Ez csak akkor lesz kibocsátva, ha a szintaxis miatt az érték eltér az alapértelmezett állapotától. Például a scoped out esetén nem bocsát ki attribútumot.
RefSafetyRulesAttribute
A C#7.2 és a C#11
- az unscoped
ref/in/outparaméterek nem a C#7.2-ben, hanem a C#11refref structmezőjeként kerülhetik el a metódushívást -
outparaméterek implicit hatókörrel vannak elosztva a C#11-ben, a C#7.2-ben pedig nem. -
ref/inparaméterek aref struct-típusokhoz implicit hatókörrel rendelkeznek a C#11-ben, és hatókör nélkül a C#7.2-ben.
A C#11-zel való újrafordításkor bekövetkező kompatibilitástörő változások esélyének csökkentése érdekében frissítjük a C#11 fordítót, hogy a metódusmeghívási
Ennek engedélyezéséhez a fordító egy új [module: RefSafetyRules(11)] attribútumot bocsát ki, amikor a modult -langversion:11-nél magasabb verzióban fordítja le, vagy olyan korlibbal fordítja le, amely tartalmazza a funkciójelzőt a ref mezők számára.
Az attribútum argumentuma a ref biztonságos környezet nyelvi verzióját jelzi, és a szabályokat, amelyeket a modul fordításakor használtak.
A verzió jelenleg 11-ra van rögzítve, függetlenül attól, hogy milyen tényleges nyelvi verziót adnak át a fordítónak.
Az elvárás az, hogy a fordító jövőbeli verziói frissítik a ref biztonságos környezet szabályait, és különböző verziókkal rendelkező attribútumokat bocsátanak ki.
Ha a fordító betölt egy olyan modult, amely nem másvalakivel 11rendelkezikversion[module: RefSafetyRules(version)], a fordító hibát jelent a nem felismert verzióhoz, ha a modulban deklarált metódusok hívása történik.
Amikor a C#11 fordító egy metódushívást elemez,:
- Ha a metódusdeklarációt tartalmazó modul tartalmazza
[module: RefSafetyRules(version)]a metódushívást,version11akkor a rendszer C#11-szabályokkal elemzi a metódushívást. - Ha a metódusdeklarációt tartalmazó modul forrásból származik, és
-langversion:11vagy arefmezők funkciójelölőjét tartalmazó corlibnal van lefordítva, a metódushívást C#11-szabályokkal elemzi a rendszer. - Ha a metódusdeklarációt tartalmazó modul
System.Runtime { ver: 7.0 }-ra hivatkozik, a metódushívást a C#11 szabályai szerint elemzik. Ez a szabály ideiglenes megoldás a C#11/.NET 7 korábbi előzetes verziójával összeállított modulok esetében, és később el lesz távolítva. - Ellenkező esetben a metódushívás C#7.2-szabályokkal lesz elemezve.
A C#11 előtti fordító figyelmen kívül hagyja a RefSafetyRulesAttribute és csak C#7.2-szabályokkal elemzi a metódushívásokat.
A RefSafetyRulesAttribute egy névtér által egyeztetett névvel lesz összekapcsolva, így a definíciónak nem szükséges megjelennie egy adott modulban sem.
A RefSafetyRulesAttribute típus csak fordítóhoz használható – a forrásban nem engedélyezett. A típusdeklarációt a fordító szintetizálja, ha még nem szerepel a fordításban.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public RefSafetyRulesAttribute(int version) { Version = version; }
public readonly int Version;
}
}
Biztonságos rögzített méretű pufferek
A biztonságos rögzített méretű puffereket nem tartalmazta a C# 11. Ez a funkció a C#egy későbbi verziójában implementálható.
A nyelv enyhíti a rögzített méretű tömbökre vonatkozó korlátozásokat, így biztonságos kódban deklarálhatók, és az elemtípus kezelhető vagy nem kezelhető. Ez jogilag elfogadhatóvá teszi az alábbi típusokat:
internal struct CharBuffer
{
internal char Data[128];
}
Ezek a deklarációk, hasonlóan a unsafe megfelelőikhez, a N elemek egy sorozatát határozzák meg a tartalmazó típuson belül. Ezek a tagok egy indexelővel érhetők el, és Span<T> és ReadOnlySpan<T> példányokra is konvertálhatók.
fixed típusú T pufferbe való indexeléskor figyelembe kell venni a tároló readonly állapotát. Ha a tároló readonly, akkor az indexelő ref readonly T ad vissza, máskülönben ref T.
A fixed puffer indexelő nélküli elérése nem rendelkezik természetes típussal, azonban Span<T> típussá konvertálható. Abban az esetben, ha a tároló readonly, a puffer implicit módon konvertálhatóvá válik ReadOnlySpan<T>, különben implicit módon átalakíthatóvá Span<T> vagy ReadOnlySpan<T> (a Span<T> átalakítás jobbnak számít).
Az eredményként kapott Span<T> példány hossza megegyezik a fixed pufferen deklarált méretével. A visszaadott érték biztonságos környezete egyenlő lesz a tároló biztonságos környezetével, ugyanúgy, mintha a háttéradatok mezőként lennének elérve.
Minden olyan fixed deklarációhoz, amelyben az elemtípus T, a nyelv létre fog hozni egy hozzá tartozó get indexelő metódust, amelynek visszatérési típusa ref T. Az indexelő a [UnscopedRef] attribútummal lesz jegyzetelve, mivel a megvalósítás a deklarálási típusú mezőket adja vissza. A tag hozzáférhetősége megegyezik a fixed mező hozzáférhetőségével.
A CharBuffer.Data indexelőjének aláírása például a következő lesz:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Ha a megadott index kívül esik a fixed tömb deklarált határain, a rendszer IndexOutOfRangeException dob. Ha állandó értéket ad meg, akkor azt a megfelelő elemre mutató közvetlen hivatkozással helyettesítjük. Kivéve, ha az állandó kívül esik a deklarált korlátokon, ebben az esetben fordítási időhiba lépne fel.
Minden fixed pufferhez létrehoznak egy elnevezett hozzáférési módot, amely érték alapú get és set műveleteket biztosít. Ez azt jelenti, hogy a fixed pufferek jobban hasonlítanak a meglévő tömbszemantikára egy ref tartozék, valamint a byval get és set műveletek révén. Ez azt jelenti, hogy a fordítók ugyanolyan rugalmasságot kapnak, amikor olyan kódokat bocsátanak ki, amelyek fixed puffereket használnak, mint tömbök használatakor. Ez megkönnyíti olyan műveletek kibocsátását, mint például a await a fixed pufferek használatával.
Ez azzal a további előnyökkel is jár, hogy a fixed pufferek más nyelvekről való felhasználása egyszerűbbé válik. A nevesített indexelők a .NET 1.0-s kiadása óta léteznek. A névvel ellátott indexelőket közvetlenül nem kibocsátó nyelvek is használhatják azokat (a C# valójában jó példa erre).
A puffer háttértárolója a [InlineArray] attribútummal jön létre. Ez a kérdés 12320- számú részében tárgyalt mechanizmus, amely kifejezetten lehetővé teszi az azonos típusú mezők sorozatának hatékony deklarálását. Ez a konkrét probléma még mindig aktív vita alatt áll, és az elvárás az, hogy ennek a funkciónak a megvalósítása a vita során is folytatódni fog.
Inicializálók ref értékekkel a new és with kifejezésekben
A 12.8.17.3 Objektum-inicializálókszakaszban a következőre frissítjük a nyelvtant:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
A with kifejezésszakaszában frissítjük a nyelvtant a következőre:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
A hozzárendelés bal oldali operandusának egy ref mezőhöz kötődő kifejezésnek kell lennie.
A jobb oldali operandusnak egy olyan kifejezésnek kell lennie, amely egy lvalue-t eredményez, amely ugyanolyan típusú értéket jelöl ki, mint a bal oldali operandus.
Hasonló szabályt adunk hozzá a helyi újraelosztási szabályhoz:
Ha a bal operandus egy írható ref (azaz a ref readonly mezőn kívül mást jelöl), akkor a jobb operandusnak írható lvalue-nak kell lennie.
A konstruktorhívások feloldási szabályai változatlanok maradnak.
A konstruktort meghívó
newkifejezés ugyanazokat a szabályokat követi, mint egy metódushívás, amely a létrehozott típust adja vissza.
A fenti metódushívás szabályai frissültek.
A metódushívás
e1.M(e2, ...)eredményeként létrejövő rvalue a legkisebb következő kontextusok közül tartalmaz biztonságos kontextust.
- A hívókörnyezet
- Az összes argumentumkifejezés hozzájárulásával létrejött biztonságos környezet
- Ha a visszatérés egy
ref struct, akkor az összes argumentum hozzájárul aref-hez.
Inicializálókkal rendelkező new kifejezések esetében az inicializáló kifejezések argumentumokként szolgálnak, mivel hozzájárulnak a biztonsági kontextushoz, míg a ref inicializáló kifejezések ref argumentumként számítanak, mert hozzájárulnak a referencia-biztonsági kontextushoz, rekurzívan.
Nem biztonságos környezet változásai
A mutatótípusok (23.3. szakasz) kiterjesztésre kerültek, hogy lehetővé tegyék a felügyelt típusok hivatkozási típusnak lenni.
Az ilyen mutatótípusokat felügyelt típusként írják, amelyet egy * token követ. Figyelmeztetést adnak ki.
A címművelet (23.6.5pont) most már elfogadja, hogy egy felügyelt típusú változót operandusként használjon.
A fixed utasítás (23.7. szakasz) módosításra került, hogy elfogadja a fixed_pointer_initializer, amely lehet egy felügyelt típusú T változó címe, vagy egy array_type kifejezés, amely felügyelt típusú Telemeket tartalmaz.
A veremfoglalás inicializálója (12.8.22) hasonlóan lazább.
Megfontolások
A funkció értékelésekor figyelembe kell venni a fejlesztési verem más részeit is.
Kompatibilitási szempontok
Ebben a javaslatban az a kihívás, hogy ennek a kialakításnak milyen kompatibilitási következményei vannak a meglévő tartomány biztonsági szabályokra, vagy §9.7.2. Bár ezek a szabályok teljes mértékben támogatják a ref struct mezőket tartalmazó ref fogalmát, a stackallockivételével nem engedélyezik az API-k számára a veremre hivatkozó ref állapot rögzítését. A ref safe környezeti szabályok szigorú feltételezéssel, vagy 8. Ez azt jelenti, hogy a biztonsági szabályok nem számolnak azzal a lehetőséggel, hogy egy ref paraméter ref mezővé válhat, ezért az alábbihoz hasonló kódot engedélyez.
Span<int> CreateSpanOfInt()
{
// This is legal according to the 7.2 span rules because they do not account
// for a constructor in the form Span(ref T value) existing.
int local = 42;
return new Span<int>(ref local);
}
Egy ref paraméternek gyakorlatilag három módja van arra, hogy elkerülje a metódushívást:
- Érték szerinti eredmény
- A
refvisszatérésig - A
refref structrefparaméterként visszaadott vagy átadott /outmezője alapján
A meglévő szabályok csak az (1) és a (2) szabályt veszik figyelembe. Ezeket (3) nem veszik figyelembe, ezért az olyan hiányosságok, mint amikor a helyiek visszatérnek ref mezők formájában, nincsenek figyelembe véve. Ennek a kialakításnak meg kell változtatnia a szabályokat, hogy figyelembe vegye a (3)-at. Ez kis hatással lesz a meglévő API-k kompatibilitására. Konkrétan az alábbi tulajdonságokkal rendelkező API-kra lesz hatással.
- Legyen
ref structaz aláírásában- Ahol a
ref structvisszatérési típus,refvagyoutparaméter - További
invagyrefparamétere van a vevő kivételével
- Ahol a
A C# 10-ben az ilyen API-k hívóinak soha nem kellett figyelembe venniük, hogy ref API állapotbemenete rögzíthető ref mezőként. Ez lehetővé tette, hogy több minta létezzen a C# 10-ben, amelyek a C# 11-ben nem lesznek biztonságosak, mivel a ref állapot ref mezőként kiszabadulhat. Például:
Span<int> CreateSpan(ref int parameter)
{
// The implementation of this method is irrelevant when considering the lifetime of the
// returned Span<T>. The ref safe context rules only look at the method signature, not the
// implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
// to escape by ref in this method
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 but would be illegal with ref fields
return CreateSpan(ref parameter);
// Legal in C# 10 but would be illegal with ref fields
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 but would be illegal with ref fields
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
Ennek a kompatibilitási törésnek a hatása várhatóan nagyon kicsi lesz. Az érintett API-alakzat kevés értelmet adott ref mezők hiányában, ezért nem valószínű, hogy az ügyfelek sok ilyent hoztak létre. Az API-struktúra meglévő tárházakban történő felismerésére szolgáló eszközöket futtató kísérletek alátámasztják ezt az állítást. Az egyetlen adattár, amely jelentős számmal rendelkezik ebből az alakból, az dotnet/runtime, és ennek az az oka, hogy az adattár képes ref mezőket létrehozni a ByReference<T> belső típus segítségével.
Ennek ellenére a kialakításnak figyelembe kell vennie az ilyen API-kat, mert érvényes mintát fejez ki, csak nem gyakori. Ezért a tervezésnek eszközöket kell biztosítania a fejlesztőknek a meglévő élettartam-szabályok visszaállításához a C# 10-re való frissítéskor. Pontosabban olyan mechanizmusokat kell biztosítania, amelyek lehetővé teszik a fejlesztők számára, hogy a ref paramétereket ref vagy ref mezők szerint nem szökhetők meg jelöljék. Ez lehetővé teszi az ügyfelek számára, hogy olyan API-kat definiáljanak a C# 11-ben, amelyek ugyanazokat a C# 10 hívási webhelyszabályokat használják.
Referencia-összeállítások
Az ebben a javaslatban ismertetett funkciókat használó összeállítás referencia-szerelvényének meg kell őriznie azokat az elemeket, amelyek biztonságos környezeti információkat közvetítenek. Ez azt jelenti, hogy az összes élettartam-megjegyzésttribútumot meg kell őrizni az eredeti helyén. Azoknak a kicserélése vagy kihagyása bármilyen kísérlete érvénytelen hivatkozási összeállításokhoz vezethet.
A ref mezők reprezentálása árnyaltabb. Ideális esetben egy ref mező jelenik meg egy referenciaszerelvényben, mint bármely más mezőben. A ref mező azonban a metaadatok formátumának módosítását jelenti, és olyan problémákat okozhat az eszközláncokkal kapcsolatban, amelyek nem frissülnek a metaadatok változásának megértéséhez. Konkrét példa a C++/CLI, amely valószínűleg hibát fog okozni, ha ref mezőt használ. Ezért előnyös, ha ref mezők kihagyhatók az alapvető kódtárak referenciaszerelvényeiből.
A ref mező önmagában nincs hatással a ref biztonságos környezet szabályaira. Konkrét példaként vegye figyelembe, hogy a meglévő Span<T> definíció ref mező használatára történő tükrözése nincs hatással a fogyasztásra. Ezért maga a ref biztonságosan elhagyható. Egy ref mező azonban más hatással van a fogyasztásra, amelyet meg kell őrizni:
- A
ref structmezővel rendelkezőrefsoha nem tekinthetőunmanaged - A
refmező típusa hatással van a végtelen általános bővítési szabályokra. Ezért ha egyrefmező típusa olyan típusparamétert tartalmaz, amelyet meg kell őrizni
Ezen szabályok figyelembevételével itt van egy érvényes szerelvény átalakítása egy ref struct-hoz:
// Impl assembly
ref struct S<T>
{
ref T _field;
}
// Ref assembly
ref struct S<T>
{
object _o; // force managed
T _f; // maintain generic expansion protections
}
Széljegyzetek
Az élettartamok természetes módon, típusokkal vannak kifejezve. Egy adott program élettartama biztonságos, ha az élettartam-típusokat ellenőrzi. Bár a C# szintaxisa implicit módon egész életeket ad hozzá az értékekhez, létezik egy mögöttes típusrendszer, amely az alapvető szabályokat írja le. Ezeket a szabályokat illetően gyakran könnyebb megvitatni a kialakítás változásainak következményeit, így ezek a szabályok a vita kedvéért itt szerepelnek.
Figyeljen arra, hogy ez nem teljes 100% dokumentáció. Itt nem az a cél, hogy minden egyes viselkedést dokumentáljon. Ehelyett egy általános megértést és általános részletesség kialakítását célozza, amellyel a modell és annak lehetséges változásai megvitathatók.
Általában nem szükséges közvetlenül beszélni az élettartam-típusokról. A kivételek olyan helyek, ahol az élettartamok az "példányosítási" helyszíntől függően változhatnak. Ez egyfajta polimorfizmus, és ezeket a változó élettartamokat "általános élettartamoknak" nevezzük, általános paraméterekként jelölve. A C# nem ad szintaxist az élettartam-generikus értékek kifejezéséhez, ezért egy implicit "fordítást" határozunk meg a C# nyelvről egy explicit generikus paramétereket tartalmazó, kibővített alacsonyabb szintű nyelvre.
Az alábbi példák nevesített élettartamokat használnak. A szintaxis $a egy anevű élettartamra hivatkozik. Ez egy olyan élet, amelynek önmagában nincs értelme, de a where $a : $b szintaxissal kapcsolatot adhat más életekhez. Ez azt állapítja meg, hogy $a átalakítható $b. Hasznos lehet úgy tekinteni erre, mint ami megállapítja, hogy $a élettartama legalább akkora, mint $b.
Az alábbiakban néhány előre meghatározott élettartamot talál a kényelem és a rövidség érdekében:
-
$heap: ez bármelyik, a halomon lévő érték élettartama. Minden környezetben és metódus-aláírásban elérhető. -
$local: ez a metódusveremen található bármely érték élettartama. Tulajdonképpen ez egy helyettesítő név a függvénytagszámára. Implicit módon definiálva van a metódusokban, és a kimeneti pozíció kivételével a metódusadákban is megjelenhet. -
$ro: névtartó csak visszatér -
$cm: helyőrző név a(z) hívás környezetéhez
Az élettartamok között van néhány előre definiált kapcsolat:
-
where $heap : $aegész életen át$a where $cm : $ro-
where $x : $localaz összes előre meghatározott élettartamra vonatkozóan. A felhasználó által meghatározott élettartamoknak nincs kapcsolatuk a helyivel, hacsak nincs explicit módon meghatározva.
A típusokon definiált élettartam-változók invariánsok vagy kovarianták lehetnek. Ezek az általános paraméterekkel megegyező szintaxissal vannak kifejezve:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
A típusdefiníciók élettartamparamétere $thisnem előre definiált, de a definiáláskor néhány szabály van hozzá társítva:
- Az első élettartam paraméternek kell lennie.
- Kovariánsnak kell lennie:
out $this. - A
refmezők élettartamának át kell alakíthatónak lennie$this-re. - A hivatkozás nélküli mezők
$thisélettartamának$heapvagy$thiskell lennie.
A ref élettartamát úgy fejezzük ki, hogy egy élettartam argumentumot adunk meg a ref-hez. Például egy ref, amely a halomra hivatkozik, így fejeződik ki: ref<$heap>.
Amikor konstruktort határoz meg a modellben, a metódus new nevet fogja használni. Szükség van egy paraméterlistára a visszaadott értékhez és a konstruktor argumentumaihoz. Ez a konstruktor bemenetei és a létrehozott érték közötti kapcsolat kifejezéséhez szükséges. A modell Span<$a><$ro> helyett inkább Span<$a> new<$ro> fog használni. A konstruktor this típusa, beleértve az élettartamokat is, a megadott visszatérési érték lesz.
Az élettartam alapvető szabályai a következők:
- Minden élettartam általános argumentumként van kifejezve, a típusargumentumok elé kerülve. Ez előre meghatározott élettartamokra igaz, kivéve
$heapés$local. - Az összes olyan
Ttípus, amely nemref struct, implicit módonT<$heap>élettartammal rendelkezik. Ez implicit, nem kell minden mintábanint<$heap>-t írni. - A
refként definiáltref<$l0> T<$l1, $l2, ... $ln>mező esetében:- Az összes élettartamnak
$l1és$lnközött invariánsnak kell lenniük. - A
$l0élettartamának kompatibilisnek kell lennie$this-ével
- Az összes élettartamnak
- A
ref, amelyref<$a> T<$b, ...>-ként van definiálva, a$b-t át kell alakítani$a-á - A változók
refélettartamát a következő határozza meg:- Egy
refhelyi, paraméter, mező vagy visszatérés típusúref<$a> Taz élettartam$a -
$heapminden referenciatípushoz és referenciatípus mezőhöz -
$localminden máshoz
- Egy
- A hozzárendelés vagy a visszatérés akkor jogszerű, ha az alapul szolgáló típusátalakítás legális
- A kifejezések élettartamát az öntvényes széljegyzetek használatával lehet explicité tenni:
-
(T<$a> expr)az érték kifejezett élettartama$aT<...> -
ref<$a> (T<$b>)expraz érték élettartama az$bT<...>és a ref élettartam$a.
-
Az élettartamra vonatkozó szabályok esetében a ref a kifejezés típusának részeként szerepel átalakítás céljából. Logikailag úgy jelenik meg, hogy ref<$a> T<...> átalakítja ref<$a, T<...>>, ahol a $a kovariáns, T pedig invariáns.
A következő lépésben definiáljuk azokat a szabályokat, amelyek lehetővé teszik a C#-szintaxis leképezését az alapul szolgáló modellre.
A rövidség kedvéért egy olyan típus, amely nem rendelkezik explicit élettartam-paraméterekkel, úgy kezelendő, mintha létezne egy out $this, amely definiálva és alkalmazva van a típus összes mezőjére. A ref mezővel rendelkező típusnak explicit élettartam-paramétereket kell meghatároznia.
Ezek a szabályok azért léteznek, hogy támogassák meglévő invariánsunkat, amely szerint T minden típus esetén hozzárendelhető scoped T-hez. Ez azt jelenti, hogy T<$a, ...> hozzárendelhető T<$local, ...>-hez minden olyan élettartam esetén, amelyről ismert, hogy átalakítható $local-re. Ez támogatja továbbá az egyéb elemeket, például Span<T> hozzárendelését a halomból a veremhez. Ez nem zárja ki azokat a típusokat, amelyekben a mezők élettartama eltérő a nem ref értékek esetében, de ez a C# mai valósága. Ennek módosítása a C#-szabályok jelentős módosítását igényelné, amelyeket le kell képezni.
A példánymetóduson belüli this típus esetén a S<out $this, ...> típusa implicit módon a következőképpen van meghatározva:
- Normál példány metódusa esetén:
ref<$local> S<$cm, ...> - Például a
[UnscopedRef]azref<$ro> S<$cm, ...>-gyel annotált módszer esetén
Az explicit this paraméter hiánya itt kényszeríti az implicit szabályokat. Összetett minták és viták esetén érdemes static metódusként írni, és this-et explicit paraméterként megadni.
ref struct S<out $this>
{
// Implicit this can make discussion confusing
void M<$ro, $cm>(ref<$ro> S<$cm> s) { }
// Rewrite as explicit this to simplify discussion
static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}
A C# metódus szintaxisa a következő módokon képezi le a modellt:
- A
refparaméterek ref élettartama$ro - A
ref structtípusú paramétereknek ez az élettartama$cm. - A ref visszatérések ref-élettartammal rendelkeznek
$ro. - A(z)
ref structtípusú visszatérések értékének élettartama$ro -
scopedparaméteren vagyrefmódosítja a referencia élettartamát$local-re
Tekintsünk át egy egyszerű példát, amely a modellt mutatja be itt:
ref int M1(ref int i) => ...
// Maps to the following.
ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
// okay: has ref lifetime $ro which is equal to $ro
return ref i;
// okay: has ref lifetime $heap which convertible $ro
int[] array = new int[42];
return ref array[0];
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Most vizsgáljuk meg ugyanezt a példát egy ref structalkalmazásával.
ref struct S
{
ref int Field;
S(ref int f)
{
Field = ref f;
}
}
S M2(ref int i, S span1, scoped S span2) => ...
// Maps to
ref struct S<out $this>
{
// Implicitly
ref<$this> int Field;
S<$ro> new<$ro>(ref<$ro> int f)
{
Field = ref f;
}
}
S<$ro> M2<$ro>(
ref<$ro> int i,
S<$ro> span1)
S<$local> span2)
{
// okay: types match exactly
return span1;
// error: has lifetime $local which has no conversion to $ro
return span2;
// okay: type S<$heap> has a conversion to S<$ro> because $heap has a
// conversion to $ro and the first lifetime parameter of S<> is covariant
return default(S<$heap>)
// okay: the ref lifetime of ref $i is $ro so this is just an
// identity conversion
S<$ro> local = new S<$ro>(ref $i);
return local;
int[] array = new int[42];
// okay: S<$heap> is convertible to S<$ro>
return new S<$heap>(ref<$heap> array[0]);
// okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These
// are convertible.
return new S<$ro>(ref<$heap> array[0]);
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Következő lépésként nézzük meg, hogyan segít ez a ciklikus önkiosztási probléma:
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
s.refField = ref s.field;
}
}
// Maps to
ref struct S<out $this>
{
int field;
ref<$this> int refField;
static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
{
// error: the types work out here to ref<$cm> int = ref<$ro> int and that is
// illegal as $ro has no conversion to $cm (the relationship is the other direction)
s.refField = ref<$ro> s.field;
}
}
Következő lépésként nézzük meg, hogyan segít ez a hülye rögzítési paraméter problémájában:
ref struct S
{
ref int refField;
void Use(ref int parameter)
{
// error: this needs to be an error else every call to this.Use(ref local) would fail
// because compiler would assume the `ref` was captured by ref.
this.refField = ref parameter;
}
}
// Maps to
ref struct S<out $this>
{
ref<$this> int refField;
// Using static form of this method signature so the type of this is explicit.
static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
{
// error: the types here are:
// - refField is ref<$cm> int
// - ref parameter is ref<$ro> int
// That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and
// hence this reassignment is illegal
@this.refField = ref<$ro> parameter;
}
}
Nyitott problémák
A kompatibilitási törések elkerülése érdekében módosítsa a kialakítást
Ez a kialakítás számos kompatibilitási törést javasol a meglévő ref-safe-context szabályokkal. Annak ellenére, hogy úgy vélik, a megszakítások minimális hatással bírnak, jelentős figyelmet szenteltek egy olyan tervezésnek, amely nem tartalmaz törési változásokat.
A kompatibilitást megőrző tervezés azonban lényegesen összetettebb volt, mint ez. Annak érdekében, hogy a ref és a ref mezők keretében történő visszatérés képessége megmaradjon, a compat ref mezőknek különböző élettartamra van szükségük. Lényegében megköveteli, hogy ref-field-safe-context nyomkövetést biztosítsunk az összes paraméterhez egy metódushoz. Ezt minden kifejezésre ki kell számítani, és gyakorlatilag minden olyan értékben nyomon kell követni, ahol ma ref-safe-context jelenik meg.
Az érték továbbá kapcsolatban áll a hivatkozás-biztonságos környezettel. Érthetetlen például, hogy egy érték visszaadható ref mezőként, de közvetlenül nem ref-ként. Ennek az az oka, hogy a ref mezőket a ref már triviálisan visszaadhatja (ref állapotát egy ref struct-ban ref akkor is visszaadhatja, ha a benne lévő érték nem adható vissza). Ezért a szabályok további állandó kiigazítást igényelnek, hogy ezek az értékek egymás szempontjából ésszerűek legyenek.
Azt is jelenti, hogy a nyelvnek szintaxisra van szüksége ref paraméterek megjelenítéséhez, amelyek három különböző módon adhatók vissza: ref mező, ref és érték szerint. Az alapértelmezett értéket refadja vissza. Előre haladva azonban arra lehet számítani, hogy a természetesebb visszatérés, különösen ha ref struct is szerepel, a ref mező vagy refáltal valósul meg. Ez azt jelenti, hogy az új API-k alapértelmezés szerint további szintaxisanotációkat igényelnek. Ez nem kívánatos.
Ezek a kompenzáló változások azonban hatással lesznek az alábbi tulajdonságokkal rendelkező metódusokra:
- Legyen egy
Span<T>vagyref struct- Ahol a
ref structvisszatérési típus,refvagyoutparaméter - További
invagyrefparamétere van (a vevő kivételével)
- Ahol a
Annak megértéséhez, hogy milyen hatással jár az API-k kategóriákra bontása:
- Azt szeretné, ha a fogyasztók tisztában lennének azzal, hogy a
refadatrefmezőként lesz rögzítve. Kiváló példa aSpan(ref T value)konstruktorokra - Ne akarják, hogy a fogyasztók úgy számoljanak, hogy a
refmezőként legyen rögzítve aref. Ezek azonban két kategóriába sorolhatók- Nem biztonságos API-k. Ezek a
UnsafeésMemoryMarshalapiS-típusok, amelyek közül aMemoryMarshal.CreateSpana legfontosabb. Ezek az API-k nem biztonságosan rögzítik aref, de ismert, hogy nem biztonságos API-k. - Biztonságos API-k. Ezek olyan API-k, amelyek
refparamétereket vesznek igénybe a hatékonyság érdekében, de valójában sehol nem rögzítik őket. A példák kicsik, de az egyikAsnDecoder.ReadEnumeratedBytes
- Nem biztonságos API-k. Ezek a
Ez a változás elsősorban a fenti (1) előnyöket biztosítja. Ezek várhatóan alkotják az API-k többségét, amelyek egy ref-t vesznek fel, és egy ref struct-et adnak vissza a jövőben. A változások negatívan befolyásolják a (2.1) és (2.2) pontokat, mivel az megszakítja a meglévő hívási szemantikát az élettartam-szabályok változása miatt.
A kategóriában (2.1) szereplő API-kat azonban nagyrészt a Microsoft vagy olyan fejlesztők készítik, akiknek leginkább hasznára válik a ref mezők használata, például a világ összes Tanneréhez hasonló fejlesztők. Ésszerű feltételezni, hogy ez a fejlesztői csoport elfogadná a C# 11-re való frissítés kompatibilitási adóját, ha cserébe kapna néhány ref mezőt. A meglévő szemantika megőrzése érdekében ez néhány megjegyzés alkalmazását igényelné.
A kategóriában (2.2) szereplő API-k a legnagyobb problémát képezik. Nem ismert, hogy hány ilyen API létezik, és nem világos, hogy ezek a harmadik féltől származó kódban gyakoribbak vagy ritkábban fordulnak-e elő. Az elvárás az, hogy nagyon kevés van belőlük, különösen, ha figyelembe vesszük a outkompatibilitási megszakítást. Az eddigi keresések nagyon kis számú ilyen meglévőt mutattak ki public felszíni területen. Ezt a mintát nehéz megkeresni, mivel szemantikai elemzést igényel. A változás megkezdése előtt eszközalapú megközelítésre lenne szükség, amely ellenőrzi az ezzel kapcsolatos feltételezéseket, ami néhány ismert esetet érint.
Bár mindkét esetben a 2. kategóriába tartozik, a javítás egyszerű. Azoknak a ref paramétereknek, amelyeket nem kívánnak elfoghatónak tekinteni, hozzá kell adniuk a scoped-et a ref-höz. A (2.1)-ben ez valószínűleg arra is kényszeríti a fejlesztőt, hogy Unsafe vagy MemoryMarshal használjon, de ez a nem biztonságos stílusú API-k esetében várható.
Ideális esetben a nyelv csökkenthetné az észrevétlen törő változások hatását, ha figyelmeztetést ad ki, amikor egy API csendesen beleesik a problémás működésbe. Ez egy olyan módszer lenne, amely ref-t vesz fel, ref struct-t ad vissza, de valójában nem fogja a ref-t a ref struct-ban. A fordító ebben az esetben diagnosztikát is kiadhat, amely tájékoztatja a fejlesztőket, hogy a ref helyett scoped ref-re kellene átalakítani.
Döntés Ezt a kialakítást el lehet érni, de az eredményül kapott funkció nehezebben használható addig a pontig, amikor a kompatinálási szünetre vonatkozó döntés született.
Döntés A fordító figyelmeztetést küld, ha egy metódus megfelel a feltételeknek, de nem rögzíti a ref paramétert ref mezőként. Ennek megfelelően figyelmeztetnie kell az ügyfeleket a frissítésre az általuk létrehozott lehetséges problémákra
Kulcsszavak és attribútumok
Ez a terv attribútumok használatát kéri az új élettartam-szabályok megjegyzéséhez. Ez ugyanolyan könnyen elvégezhető lett volna környezetfüggő kulcsszavakkal is. Például a [DoesNotEscape] térképezhető scoped-re. Azonban a kulcsszavaknak, még a környezetfüggőknek is, általában meg kell felelniük nagyon magas követelményeknek a felvételhez. Értékes helyet foglalnak el a nyelvben, és a nyelv hangsúlyosabb részei. Ez a funkció értékes ugyan, de a C#-fejlesztők kisebbségét fogja szolgálni.
A felületen elsőre úgy tűnhet, hogy nem érdemes kulcsszavakat használni, de két fontos szempontot kell figyelembe venni:
- A jegyzetek befolyásolják a program szemantikáját. Az attribútumoknak a programszemantikára gyakorolt hatása az a határ, amelyet a C# vonakodik átlépni, és nem világos, hogy ez az-e a tulajdonság, amely indokolná ezt a lépést.
- A legvalószínűbb, hogy az ezt a funkciót használó fejlesztők csoportja nagymértékben átfedésben van a függvénymutatókat használó fejlesztők halmazával. Ez a funkció, bár a fejlesztők kisebbsége is használja, új szintaxist igényelt, és ez a döntés továbbra is helyesnek tekinthető.
Ez azt jelenti, hogy a szintaxist figyelembe kell venni.
A szintaxis vázlata a következő:
-
[RefDoesNotEscape]ascoped ref-re vonatkozik -
[DoesNotEscape]ascoped-re vonatkozik -
[RefDoesEscape]aunscoped-re vonatkozik
döntésiscoped és scoped refszintaxisának használata; unscopedattribútum használata.
Rögzített puffer lokális engedélyezése
Ez a kialakítás biztonságos fixed puffereket tesz lehetővé, amelyek bármilyen típust támogatnak. Itt az egyik lehetséges bővítmény lehetővé teszi az ilyen fixed pufferek helyi változóként való deklarálását. Ez lehetővé tenné számos meglévő stackalloc művelet fixed pufferrel való cseréjét. Emellett kibővítené azon forgatókönyvek körét, amelyekben veremalapú foglalások lehetségesek, mivel a stackalloc csak nem felügyelt elemtípusokra korlátozódik, míg a fixed pufferek nincsenek ilyen megkötéssel.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
Ez összetart, de szükséges, hogy kissé kiterjesszük a szintaxist a lokális változók számára. Nem világos, hogy ez nem éri-e meg a további összetettség. Lehetséges, hogy egyelőre nemet dönthetünk, és később visszahozhatjuk, ha elegendő igény mutatkozik.
Példa arra, hogy ez hol lenne előnyös: https://github.com/dotnet/runtime/pull/34149
határozat, egy időre halasszák el ezt
Modreqs használata vagy nem
Arról kell dönteni, hogy az új élettartam-attribútumokkal megjelölt módszereket le kell-e fordítani modreq-ra a kibocsátás során. Ha ezt a megközelítést alkalmazná, a széljegyzetek és modreq között gyakorlatilag 1:1-alapú leképezés lenne.
A modreq hozzáadásának indoka az, hogy az attribútumok módosítják a ref safe környezeti szabályok szemantikáját. Csak azok a nyelvek hívhatják meg a szóban forgó módszereket, amelyek megértik ezeket a szemantikát. Továbbá az OHI-forgatókönyvekre alkalmazva az élettartamok szerződéssé válnak, amelyet minden származtatott módszernek végre kell hajtania. Ha a széljegyzetek modreq nélkül léteznek, az olyan helyzetekhez vezethet, amikor virtual metódusláncok ütköző élettartamú széljegyzetekkel vannak betöltve (akkor fordulhat elő, ha virtual láncnak csak egy része van lefordítva, a másik pedig nem).
A kezdeti ref biztonságos környezet munkája nem a modreq-t használta, hanem a nyelvekre és a keretrendszerre támaszkodott a megértés érdekében. Ugyanakkor a ref biztonságos környezet szabályaihoz hozzájáruló összes elem erős részét képezi a módszer aláírásának: ref, in, ref structstb. Ezért a metódus meglévő szabályainak módosítása már binárisan módosítja az aláírást. Ahhoz, hogy az új élethosszig tartó annotációk ugyanolyan hatással legyenek, modreq végrehajtásra van szükség.
Az a kérdés, hogy ez túlzás-e. Ez negatív hatással van arra, hogy az aláírások rugalmasabbá tétele, például a [DoesNotEscape] hozzáadása egy paraméterhez, bináris kompatibilitás-változást eredményez. Ez a kompromisszum azt jelenti, hogy idővel a BCL-hez hasonló keretrendszerek valószínűleg nem fogják tudni enyhíteni az ilyen jellegű aláírásokat. Bizonyos mértékig enyhíthető, ha a nyelv in paramétereket használ, és csak virtuális pozíciókban alkalmazza modreq.
Határozat Ne használja a modreq a metaadatokban. A out és a ref közötti különbség nem modreq, de most már különböző ref biztonságos környezeti értékekkel rendelkeznek. Nincs valódi előnye annak, ha a szabályokat csak félig betartatják itt modreq-nál.
Többdimenziós rögzített pufferek engedélyezése
Ki kell terjeszteni a fixed pufferek kialakítását többdimenziós stílustömbökre? Lényegében az alábbihoz hasonló deklarációk engedélyezése:
struct Dimensions
{
int array[42, 13];
}
Határozat Egyelőre ne engedélyezze
A hatókör megszegése
A futtatókörnyezeti adattár számos nem nyilvános API-val rendelkezik, amelyek ref mezőként rögzítik ref paramétereket. Ezek nem biztonságosak, mert az eredményül kapott érték élettartamát nem követi nyomon a rendszer. Például a Span<T>(ref T value, int length) konstruktor.
Ezeknek az API-knak a többsége valószínűleg azt választja, hogy a megfelelő élettartam-nyomon követést biztosítsa az eredmény esetén, amit egyszerűen a C# 11-es verziójára frissítve érhet el. Néhányan azonban meg szeretnék tartani a jelenlegi szemantikát, hogy ne kövessék nyomon a visszatérési értéket, mert az egész szándékuk célja, hogy ne legyenek biztonságosak. A legfontosabb példák a MemoryMarshal.CreateSpan és a MemoryMarshal.CreateReadOnlySpan. Ez a paraméterek scopedként való megjelölésével érhető el.
Ez azt jelenti, hogy a futtatókörnyezetnek szüksége van egy már meglévő mintára a scoped egy paraméterből való biztonságos eltávolítására.
-
Unsafe.AsRef<T>(in T value)bővíthetné meglévő célját, hascoped in T value- ra váltana. Ez lehetővé tenné számára, hogy mindin, mindscopedeltávolítsa a paraméterekből. Ezután az univerzális "ref safety eltávolítása" módszerré válik. - Új metódus bevezetése, amelynek teljes célja az
scopedeltávolítása:ref T Unsafe.AsUnscoped<T>(scoped in T value). Ez eltávolítja ain-t is, mert ha ez nem történne meg, akkor a hívóknak továbbra is több metódushívásra lenne szükségük a "referenciabiztonság" eltávolításához, ekkor a meglévő megoldás valószínűleg elegendő lenne.
Ezt alapértelmezés szerint leválasztja?
A kialakításnak csak két helye van, amelyek alapértelmezés szerint scoped:
-
thisscoped refvan -
outscoped refvan
A out döntése az ref mezők kompatibilitási terhelésének jelentős csökkentése, és ez egyszersmind természetesebb alapértelmezés. Lehetővé teszi a fejlesztők számára, hogy a out-t kizárólag kifelé áramló adatokként képzeljék el, míg ha ref, akkor a szabályoknak mindkét irányban figyelembe kell venniük az adatok áramlását. Ez jelentős fejlesztői zavart okoz.
A this döntése nem kívánatos, mert azt jelenti, hogy egy struct mezőt nem tud visszaadni ref. Ez egy fontos forgatókönyv a magas szintű fejlesztők számára, és a [UnscopedRef] attribútum lényegében ehhez az egy forgatókönyvhöz lett hozzáadva.
A kulcsszavak magas mércét jelentenek, és gyanús, ha csak egyetlen forgatókönyvhöz adjuk hozzá. Fölvetődött annak a gondolata, hogy teljesen kihagyjuk ezt a kulcsszót azáltal, hogy alapértelmezés szerint this egyszerűen ref, és ne scoped reflegyen. Minden olyan tag megteheti, aki azt szeretné, hogy this legyen scoped ref, a metódus scoped megjelölésével (mivel a metódus ma readonly lehet megjelölve a readonly ref létrehozásához).
Egy normál struct esetén ez többnyire pozitív változás, mivel csak akkor vezet be kompatibilitási problémákat, ha egy tagnak ref visszatérése van. Nagyon kevés ezekből a
Egy ref struct alatt ez a változás jelentősen nagyobb kompatibilitási problémákat okoz. Vegye figyelembe a következőket:
ref struct Sneaky
{
int Field;
ref int RefField;
public void SelfAssign()
{
// This pattern of ref reassign to fields on this inside instance methods would now
// completely legal.
RefField = ref Field;
}
static Sneaky UseExample()
{
Sneaky local = default;
// Error: this is illegal, and must be illegal, by our existing rules as the
// ref-safe-context of local is now an input into method arguments must match.
local.SelfAssign();
// This would be dangerous as local now has a dangerous `ref` but the above
// prevents us from getting here.
return local;
}
}
Ez lényegében azt jelentené, hogy módosíthatóref struct lokális változók összes példánymetódus-meghívása illegális lenne, kivéve, ha a lokális további scoped-ként lett jelölve. A szabályoknak figyelembe kell venniük azt az esetet, amikor a mezőket újból hozzárendelték thismás mezőihez. A readonly ref struct nem okoz ilyen problémát, mert a readonly természete megakadályozza az újbóli hozzárendelést. Ez még mindig jelentős visszafelé kompatibilitási változás lenne, mivel gyakorlatilag minden meglévő módosítható ref struct-ra hatással lenne.
A readonly ref struct még mindig problémás, amikor további ref mezőket adunk hozzá a ref struct-höz. Ez ugyanazt az alapproblémát oszlatja el azáltal, hogy a rögzítési folyamatot a ref mező értékébe helyezi át.
readonly ref struct ReadOnlySneaky
{
readonly int Field;
readonly ref ReadOnlySpan<int> Span;
public void SelfAssign()
{
// Instance method captures a ref to itself
Span = new ReadOnlySpan<int>(ref Field, 1);
}
}
Fontolóra vették, hogy a this más alapértelmezett beállításokat alkalmazzanak a struct vagy a tag típusától függően. Például:
-
thisref:struct,readonly ref structvagyreadonly member -
thismintscoped ref:ref structvagyreadonly ref structarefmezővelref struct
Ez minimalizálja a kompatibilitási töréseket, és maximalizálja a rugalmasságot, de az ügyfelek számára bonyolítja a történetet. Emellett nem oldja meg teljesen a problémát, mert a jövőbeli funkciók, például a biztonságos fixed bufferek, megkövetelik, hogy egy módosítható ref structref visszatérítéseket biztosítson olyan mezőkre, amelyek nem működnek megfelelően önmagában ezzel a kialakítással, mivel a scoped ref kategóriába tartoznának.
határozat megtartja thisscoped ref. Ez azt jelenti, hogy az előző sunyi példák fordítási hibákat eredményeznek.
ref mezők ref struktúrává
Ez a funkció új, ref biztonságos környezeti szabályokat vezet be, mivel lehetővé teszi, hogy egy ref mező egy ref struct-re hivatkozzon. A ByReference<T> általános jellege azt jelentette, hogy a futtatókörnyezetnek eddig nem volt ilyen felépítése. Ennek eredményeképpen minden szabályunk meg van írva, feltételezve, hogy ez nem lehetséges. A ref mező funkciója nagyrészt nem az új szabályok létrehozásáról, hanem a rendszerünkben meglévő szabályok kodifikálásáról szól. A ref mezők ref struct tétele új szabályok kidolgozását igényli, mivel több új forgatókönyvet is figyelembe kell venni.
Az első az, hogy egy readonly ref képes ref állapot tárolására. Például:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Ez azt jelenti, hogy amikor a metódusargumentumokra gondolunk, és a szabályoknak való megfelelést mérlegeljük, figyelembe kell vennünk, hogy readonly ref T lehet a metódus kimenete, ha T esetleg tartalmaz egy ref mezőt egy ref struct-hez.
A második probléma az, hogy a nyelvnek új típusú biztonságos környezetet kell figyelembe vennie: ref-field-safe-context. Minden ref struct, amely tranzitív módon tartalmaz egy ref mezőt, egy másik menekülési hatókörrel rendelkezik, amely a ref mező(ek) értékét jelöli. Több ref mező esetén ezek együttesen egyetlen értékként követhetők nyomon. A paraméterek alapértelmezett értéke hívókörnyezet.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Ez az érték nem kapcsolódik a tároló biztonságos környezet-hez; mivel ahogy a tárolókörnyezete egyre kisebb lesz, ez nincs hatással a mezőértékek ref-jára. Továbbá a ref-field-safe-context soha nem lehet kisebb, mint a tároló biztonságos környezete.
ref struct Nested
{
ref Span<int> Span;
}
void M(ref Nested nested)
{
scoped ref Nested refLocal = ref nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is illegal
refLocal.Span = stackalloc int[42];
scoped Nested valLocal = nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is still illegal
valLocal.Span = stackalloc int[42];
}
Ez a ref-field-safe-context lényegében mindig is létezett. Eddig a ref mezők csak a normál struct-re mutathattak, ezért könnyedén összeolvadtak a hívókörnyezet-mal. A meglévő szabályok refref struct mezők támogatásához frissíteni kell ezt az új ref-safe-context.
Harmadszor, a referencia újrakiosztási szabályokat frissíteni kell, hogy ne sértsük meg ref-field-context az értékekhez. Lényegében olyan x.e1 = ref e2 esetében, ahol a e1 típusa ref struct a ref-field-safe-context egyenlőnek kell lennie.
Ezek a problémák nagyon megoldhatóak. A fordítócsapat felvázolta ezeknek a szabályoknak néhány verzióját, és nagyrészt kiesnek a meglévő elemzésből. A probléma az, hogy nincs olyan kód, amely ezeket a szabályokat fogyasztaná, és amely segítene bizonyítani azok helyességét és használhatóságát. Emiatt nagyon tétovázunk támogatást nyújtani ehhez, mert attól tartunk, hogy rossz alapértelmezett beállításokat választunk, és a futtatókörnyezetet a használhatósági zsákutcába tesszük, amikor kihasználja ezt. Ez az aggodalom különösen erős, mert a .NET 8 valószínűleg ebbe az irányba terel minket allow T: ref struct és Span<Span<T>>. A szabályok jobban megírhatók, ha a használati kóddal együtt történik.
Döntés Késleltetés, amely lehetővé teszi, hogy a ref mező ref struct a .NET 8-ig, ahol olyan forgatókönyvek vannak, amelyek segítenek a szabályok kidolgozásában ezen forgatókönyvek körül. Ez .NET 9-ben nincs megvalósítva.
Mi teszi a C# 11.0-t?
A dokumentumban ismertetett funkciókat nem kell egyetlen lépésben implementálnunk. Ehelyett az alábbi gyűjtőkben több nyelvi kiadás fázisaiban implementálhatók:
-
refmezők ésscoped [UnscopedRef]-
refmezőkrőlref structmezőkre - Korlátozott naplementetípusok
- rögzített méretű pufferek
Az, hogy melyik kiadásban lesz implementálva, az csupán egy hatókörkezelési gyakorlat.
határozat csak (1) és (2) készült C# 11.0. A többi a C#későbbi verzióiban lesz figyelembe véve.
Jövőbeli szempontok
Fejlett élettartam-jelölések
Az ebben a javaslatban szereplő élettartam-megjegyzések korlátozottak abban a tekintetben, hogy lehetővé teszik a fejlesztők számára az értékek alapértelmezett megőrző/nem megőrző viselkedésének módosítását. Ez nagy rugalmasságot ad a modellnek, de nem változtatja meg gyökeresen a kifejezhető kapcsolatok készletét. A C#-modell lényegében még mindig bináris: visszaadható-e egy érték, vagy sem?
Ez lehetővé teszi, hogy megértsük a korlátozott élettartamú kapcsolatokat. Egy metódusból nem visszaadható érték például rövidebb élettartamú, mint egy metódusból visszaadható érték. A metódusból visszaadható értékek közötti élettartam-kapcsolatot azonban nem lehet leírni. Pontosabban nem lehet azt mondani, hogy az egyik érték élettartama nagyobb, mint a másik, ha létrejött, mindkettő visszaadható egy metódusból. Az életünk fejlődésének következő lépése az lenne, hogy lehetővé teszi az ilyen kapcsolatok leírását.
Más módszerek, például a Rust lehetővé teszik az ilyen típusú kapcsolatok kifejezését, és így összetettebb scoped stílusműveleteket valósíthatnak meg. A nyelvünk hasonlóképpen előnyös lehet, ha egy ilyen funkciót tartalmaz. Jelenleg nincs motiváló nyomás arra, hogy ezt tegyük, de ha a jövőben mégis lenne, a scoped modell bővíthető lenne, hogy ezt meglehetősen egyszerű módon belefoglalja.
Minden scoped hozzárendelhető egy elnevezett élettartamhoz egy általános stílusargumentum hozzáadásával a szintaxishoz. A scoped<'a> például egy olyan érték, amelynek élettartama 'a. A where-hoz hasonló megkötések aztán az ilyen élettartamok közötti kapcsolatok leírására használhatók.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Ez a módszer két élettartamot határoz meg 'a és 'b, valamint a kapcsolatukat, különösen azt, hogy 'b nagyobb, mint 'a. Ez lehetővé teszi, hogy a hívási helyek részletesebb szabályokkal rendelkezzenek arra vonatkozóan, hogyan lehet biztonságosan átadni az értékeket a metódusoknak, szemben a ma előforduló durva szemcsésebb szabályokkal.
Kapcsolódó információk
Kérdések
A következő problémák mind ehhez a javaslathoz kapcsolódnak:
- https://github.com/dotnet/csharplang/issues/1130
- https://github.com/dotnet/csharplang/issues/1147
- https://github.com/dotnet/csharplang/issues/992
- https://github.com/dotnet/csharplang/issues/1314
- https://github.com/dotnet/csharplang/issues/2208
- https://github.com/dotnet/runtime/issues/32060
- https://github.com/dotnet/runtime/issues/61135
- https://github.com/dotnet/csharplang/discussions/78
Javaslatok
A következő javaslatok kapcsolódnak ehhez a javaslathoz:
Meglévő minták
Ez az adott kódrészlet nem biztonságos, mert problémákba ütközik egy Span<T> áthaladásával kapcsolatban, amelyet egy ref structpéldánymetódusához lehet lefoglalni. Annak ellenére, hogy ez a paraméter nincs rögzítve, a nyelvnek feltételeznie kell, hogy az, és ezért szükségtelenül súrlódást okoz itt.
Ez a kódrészlet egy paramétert szeretne mutálni az adatok elemeinek szökésével. A kiszabadult adatok a veremre foglalhatók a hatékonyság érdekében. Annak ellenére, hogy a paraméter nincs elkerülve, a fordító biztonságos környezet rendel hozzá a beágyazási módszeren kívülről, mivel ez egy paraméter. Ez azt jelenti, hogy a veremfoglalás használatához az implementációnak unsafe-t kell használnia, hogy az adatok kikerülése után vissza tudja rendelni azokat a paraméterhez.
Szórakoztató minták
ReadOnlySpan<T>
public readonly ref struct ReadOnlySpan<T>
{
readonly ref readonly T _value;
readonly int _length;
public ReadOnlySpan(in T value)
{
_value = ref value;
_length = 1;
}
}
Szabályszerű lista
struct FrugalList<T>
{
private T _item0;
private T _item1;
private T _item2;
public int Count = 3;
public FrugalList(){}
public ref T this[int index]
{
[UnscopedRef] get
{
switch (index)
{
case 0: return ref _item0;
case 1: return ref _item1;
case 2: return ref _item2;
default: throw null;
}
}
}
}
Példák és jegyzetek
Az alábbiakban néhány példát mutatunk be, amelyek bemutatják, hogyan és miért működnek a szabályok a működésük szerint. Néhány példa a veszélyes viselkedésre, és arra, hogy a szabályok hogyan akadályozhatják meg őket. Ezeket fontos szem előtt tartani a javaslat módosításakor.
Referencia-újrahívás és hívási helyek
Annak bemutatása, hogy újra-hozzárendelési és metódushívás hogyan működik együtt.
ref struct RS
{
ref int _refField;
public ref int Prop => ref _refField;
public RS(int[] array)
{
_refField = ref array[0];
}
public RS(ref int i)
{
_refField = ref i;
}
public RS CreateRS() => ...;
public ref int M1(RS rs)
{
// The call site arguments for Prop contribute here:
// - `rs` contributes no ref-safe-context as the corresponding parameter,
// which is `this`, is `scoped ref`
// - `rs` contribute safe-context of *caller-context*
//
// This is an lvalue invocation and the arguments contribute only safe-context
// values of *caller-context*. That means `local1` has ref-safe-context of
// *caller-context*
ref int local1 = ref rs.Prop;
// Okay: this is legal because `local` has ref-safe-context of *caller-context*
return ref local1;
// The arguments contribute here:
// - `this` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `this` contributes safe-context of *caller-context*
//
// This is an rvalue invocation and following those rules the safe-context of
// `local2` will be *caller-context*
RS local2 = CreateRS();
// Okay: this follows the same analysis as `ref rs.Prop` above
return ref local2.Prop;
// The arguments contribute here:
// - `local3` contributes ref-safe-context of *function-member*
// - `local3` contributes safe-context of *caller-context*
//
// This is an rvalue invocation which returns a `ref struct` and following those
// rules the safe-context of `local4` will be *function-member*
int local3 = 42;
var local4 = new RS(ref local3);
// Error:
// The arguments contribute here:
// - `local4` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `local4` contributes safe-context of *function-member*
//
// This is an lvalue invocation and following those rules the ref-safe-context
// of the return is *function-member*
return ref local4.Prop;
}
}
Újra-hozzárendelés és nem biztonságos menekülések
Az hozzárendelési szabályok következő sorának oka első pillantásra nem biztos, hogy nyilvánvaló.
e1ugyanolyan biztonságos környezettel kell rendelkeznie, minte2
Ennek az az oka, hogy az ref helyek által mutatott értékek élettartama invariáns. A közvetettség megakadályozza, hogy bármilyen varianciát engedélyezzünk itt, még a szűkebb élettartamokig is. Ha a szűkítés engedélyezett, a következő nem biztonságos kódot nyitja meg:
void Example(ref Span<int> p)
{
Span<int> local = stackalloc int[42];
ref Span<int> refLocal = ref local;
// Error:
// The safe-context of refLocal is narrower than p. For a non-ref reassignment
// this would be allowed as its safe to assign wider lifetimes to narrower ones.
// In the case of ref reassignment though this rule prevents it as the
// safe-context values are different.
refLocal = ref p;
// If it were allowed this would be legal as the safe-context of refLocal
// is *caller-context* and that is satisfied by stackalloc. At the same time
// it would be assigning through p and escaping the stackalloc to the calling
// method
//
// This is equivalent of saying p = stackalloc int[13]!!!
refLocal = stackalloc int[13];
}
Ha egy ref nem ref struct, ez a szabály triviálisan teljesül, mivel az értékek mindegyike ugyanazzal a biztonságos környezettel. Ez a szabály valójában csak akkor lép életbe, ha az érték egy ref struct.
A ref viselkedése fontos lesz a jövőben, ahol lehetővé tesszük a ref mezők számára, hogy ref struct.
hatókörbe tartozó helyiek
A helyi scoped használata különösen hasznos lehet olyan kódsablonokban, amelyek feltételesen rendelnek értékeket a helyiekhez különböző biztonságos környezettel rendelkező-ből. Ez azt jelenti, hogy a kódnak már nem kell inicializálási trükkökre támaszkodnia, például = stackalloc byte[0] a helyi biztonságos környezet meghatározásához, de most már egyszerűen használhatja a scoped.
// Old way
// Span<byte> span = stackalloc byte[0];
// New way
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
span = stackalloc byte[len];
}
else
{
span = new byte[len];
}
Ez a minta gyakran jelenik meg alacsony szintű kódban. Ha az érintett ref structSpan<T>, akkor használható a fenti trükk. Más ref struct típusokra azonban nem alkalmazható, és előfordulhat, hogy az alacsony szintű kódnak unsafe-re kell támaszkodnia az élettartam helyes megadásának képtelensége miatt.
hatókörben megadott paraméterértékek
Az alacsony szintű kód ismétlődő súrlódásának egyik forrása a paraméterek alapértelmezett feloldása megengedő. Az biztonságos kontextus a hívókörnyezetszámára. Ez egy ésszerű alapértelmezett beállítás, mivel a .NET egészének kódolási mintáival egybeesik. Alacsony szintű kód esetén azonban nagyobb a ref struct használata, és ez az alapértelmezett beállítás súrlódást okozhat a ref biztonságos környezet szabályainak más részeivel.
A fő súrlódási pont azért fordul elő, mert a metódus argumentumainak meg kell egyeznie szabálynak. Ez a szabály leggyakrabban az ref struct példánymódszereivel lép életbe, ahol legalább egy paraméter is ref struct. Ez egy gyakori minta az alacsony szintű kódban, ahol ref struct típusok gyakran használják Span<T> paramétereket a metódusaikban. Például minden olyan íróstíluson ref struct, amely Span<T> használ a pufferek megkerüléséhez.
Ez a szabály az alábbihoz hasonló forgatókönyvek megelőzésére létezik:
ref struct RS
{
Span<int> _field;
void Set(Span<int> p)
{
_field = p;
}
static void DangerousCode(ref RS p)
{
Span<int> span = stackalloc int[] { 42 };
// Error: if allowed this would let the method return a reference to
// the stack
p.Set(span);
}
}
Lényegében ez a szabály azért létezik, mert a nyelvnek feltételeznie kell, hogy egy metódus összes bemenete a maximálisan megengedett biztonságos környezetbe () kerül ref vagy out paraméterek, beleértve a fogadókat is, lehetséges, hogy a bemenetek a ref értékek mezőiként kimenjenek (ahogyan ez a fent említett RS.Set esetében történik).
A gyakorlatban azonban számos olyan módszer létezik, amelyek ref struct olyan paraméterekként adják át, amelyek soha nem kívánják rögzíteni őket a kimenetben. Ez csak az aktuális metódusban használt érték. Például:
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Error: The safe-context of `span` is function-member
// while `reader` is outside function-member hence this fails
// by the above rule.
if (reader.TextEquals(span))
{
...
}
}
}
Annak érdekében, hogy ezt az alacsony szintű kódot használva csaljanak, unsafe trükköket alkalmaznak, hogy megtévesszék a fordítót a ref structélettartamával kapcsolatban. Ez jelentősen csökkenti a ref struct értékajánlatát, hiszen ezek célja az, hogy elkerüljék a unsafe-et, miközben továbbra is nagy teljesítményű kódot lehet írni.
Itt a scoped egy hatékony eszköz a ref struct paraméterekre, mert elveti őket mint visszatérési értékeket a metódusból a frissített metódusargumentumoknak meg kell felelnie aszabálynak. A ref struct használt, de soha vissza nem adott paraméter scoped címkézhető, hogy rugalmasabbá tegye a híváswebhelyeket.
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(scoped ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Okay: the compiler never considers `span` as capturable here hence it doesn't
// contribute to the method arguments must match rule
if (reader.TextEquals(span))
{
...
}
}
}
Annak megakadályozása, hogy a trükkös újrafelosztás könnyen mutálódni tudjon
Ha egy ref a readonly mezőjébe kerül egy konstruktorban vagy tag init esetén, a típus ref nem ref readonly. Ez egy hosszú ideig fennálló viselkedés, amely lehetővé teszi az alábbihoz hasonló kód használatát:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
Ez azonban problémát jelenthet, ha egy ilyen ref egy azonos típusú ref mezőbe lehetne tárolni. Lehetővé tenné egy readonly struct közvetlen mutációját egy példánytagtól:
readonly ref struct S
{
readonly int i;
readonly ref int r;
public S()
{
i = 0;
// Error: `i` has a narrower scope than `r`
r = ref i;
}
public void Oops()
{
r++;
}
}
A javaslat azonban megakadályozza ezt, mert megsérti a ref biztonságos környezet szabályait. Vegye figyelembe a következőket:
- A
thisa függvénytag, és a safe-context a hívókörnyezeti. Ezek mind athisszabványai egystructtagban. - A
ifüggvénytag. Ez kiesik a mező élettartamára vonatkozó szabályokból. Pontosabban a 4. szabály.
Ezen a ponton a r = ref i sor a ref újra-hozzárendelési szabályok szerint jogellenes.
Ezek a szabályok nem célja, hogy megakadályozzák ezt a viselkedést, de ennek ellenére mégis mellékhatásként ezt teszik. Ezt fontos szem előtt tartani minden jövőbeli szabályfrissítésnél, hogy értékelje az ilyen helyzetekre gyakorolt hatást.
Buta ciklikus hozzárendelés
A tervezés egyik aspektusa, amellyel küzdött, az, hogy milyen szabadsággal lehet egy ref-et visszaadni egy metódusból. Az összes ref visszaadása a normál értékekhez hasonlóan szabadon lehetséges, ami valószínűleg a legtöbb fejlesztő számára intuitív módon elvárt. Lehetővé teszi azonban azokat a patológiás forgatókönyveket, amelyeket a fordítónak figyelembe kell vennie a referencia biztonság kiszámításakor. Vegye figyelembe a következőket:
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
// Error: s.field can only escape the current method through a return statement
s.refField = ref s.field;
}
}
Ez nem olyan kódminta, amelyet minden fejlesztőtől elvárunk. Ha azonban egy ref ugyanolyan élettartammal adható vissza, mint egy érték, az a szabályok szerint legális. A fordítónak minden jogi esetet figyelembe kell vennie egy metódushívás kiértékelésekor, és ez ahhoz vezet, hogy az ilyen API-k gyakorlatilag használhatatlanok.
void M(ref S s)
{
...
}
void Usage()
{
// safe-context to caller-context
S local = default;
// Error: compiler is forced to assume the worst and concludes a self assignment
// is possible here and must issue an error.
M(ref local);
}
Az API-k használhatóvá tétele érdekében a fordító biztosítja, hogy egy ref paraméter ref élettartama kisebb legyen, mint a társított paraméterértékben lévő hivatkozások élettartama. Ez az indoklás amellett, hogy ref-safe-context a ref-től a ref struct-ig csak visszatérési legyen, és outhívókörnyezetilegyen. Ez megakadályozza a ciklikus hozzárendelést az élettartamok különbsége miatt.
Vegye figyelembe, hogy [UnscopedRef] a refref struct értékeket hívókörnyezeti, így lehetővé teszi a ciklikus hozzárendelést, és a híváslánc [UnscopedRef] vírusos használatát kényszeríti ki:
S F()
{
S local = new();
// Error: self assignment possible inside `S.M`.
S.M(ref local);
return local;
}
ref struct S
{
int field;
ref int refField;
public static void M([UnscopedRef] ref S s)
{
// Allowed: s has both safe-context and ref-safe-context of caller-context
s.refField = ref s.field;
}
}
Hasonlóképpen a [UnscopedRef] out lehetővé teszi a ciklikus hozzárendelést, mivel a paraméter rendelkezik mind biztonságos környezettel, mind ref-biztonságos környezettel, ami csak visszatérési kontextus.
A [UnscopedRef] ref előléptetése a híváskörnyezet-be hasznos lehet, ha a típus nemref struct (Figyelembe kell venni, hogy szeretnénk a szabályokat egyszerűnek tartani, hogy ne tegyenek különbséget a ref-ek és a nem ref szerkezetek között).
int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2
static S F([UnscopedRef] ref int x)
{
S local = new();
local.M(ref x);
return local;
}
ref struct S
{
public ref int RefField;
public void M([UnscopedRef] ref int data)
{
RefField = ref data;
}
}
A fejlett annotációk tekintetében a [UnscopedRef] terv a következők létrehozását eredményezi:
ref struct S { }
// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)
// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
where 'b >= 'a
Csak olvasható nem lehet mélységében a ref mezőkön keresztül.
Tekintse meg az alábbi kódmintát:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
A ref példányok readonly mezőire vonatkozó szabályok vákuumban való tervezésekor a szabályok érvényesen úgy tervezhetők, hogy a fentiek megengedettek vagy tiltottak legyenek. A readonly lényegében érvényesen lehet mélyen egy ref mezőben, vagy alkalmazható csak a ref-re. Ha csak a ref alkalmazza, az megakadályozza az újbóli hozzárendelést, de lehetővé teszi a normál hozzárendelést, amely módosítja a hivatkozott értéket.
Ez a kialakítás azonban nem létezik vákuumban, hanem olyan típusokra vonatkozó szabályokat tervez, amelyek már hatékonyan rendelkeznek ref mezőkkel. A legszembetűnőbb ezek közül, Span<T>, már erős függőséget mutat arra, hogy readonly ne legyen mély itt. Elsődleges forgatókönyve a ref mezőhöz való hozzárendelés lehetősége egy readonly-példányon keresztül.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Ez azt jelenti , hogy a readonlysekély értelmezését kell választanunk .
Konstruktorok modellezése
Egy apró tervezési kérdés: Hogyan modellezhetők a konstruktorok felépítése a ref biztonság érdekében? Alapvetően hogyan történik a következő konstruktor elemzése?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Nagyjából két megközelítés létezik:
- Modell
staticmódszerként, ahol athishelyi, és ahol a biztonságos környezet a hívókörnyezet. - Modell
staticmetódusként, ahol athisegyoutparaméter.
A konstruktornak továbbá a következő invariánsoknak kell megfelelnie:
- Győződjön meg arról, hogy
refparaméterek rögzíthetőkrefmezőkként. - Győződjön meg arról, hogy a
refmezői athismezőkhöz nem lehetnek arefparaméterekkel kimenekítve. Ez megsértené trükkös ref-hozzárendelést.
A szándék az, hogy a konstruktorokra vonatkozó speciális szabályok bevezetése nélkül válasszuk ki azt a formát, amely kielégíti az invariánsainkat. Tekintve, hogy a konstruktorok számára a legjobb modell az, ha a this-t out paraméterként tekintjük. A természetét adja vissza, lehetővé teszi számunkra, hogy különleges burkolat nélkül kielégítsük a fenti összes invariánst:
public static void ctor(out S @this, ref int f)
{
// The ref-safe-context of `ref f` is *return-only* which is also the
// safe-context of `this.field` hence this assignment is allowed
@this.field = ref f;
}
A metódusargumentumoknak egyeznie kell
A metódusargumentumoknak egyezniük kell a szabályokkal, ami gyakran összezavarja a fejlesztőket. Ez egy olyan szabály, amely számos speciális esettel rendelkezik, amelyeket nehéz megérteni, hacsak nem ismeri a szabály mögötti érvelést. A szabályok okainak jobb megértése érdekében leegyszerűsítjük a ref-biztonsági környezet és a biztonsági környezet fogalmakat egyszerűen környezetfogalomra.
A metódusok elég liberálisan visszaadhatják a nekik paraméterként átadott állapotot. Lényegében minden olyan elérhető állapot visszaadható, amely nem korlátozott (beleértve a refáltali visszatérést is). Ez közvetlenül egy return utasítással vagy közvetve is visszaadható egy ref értékhez rendelve.
A közvetlen visszatérések nem jelentenek sok problémát a referenciabiztonság szempontjából. A fordítónak egyszerűen meg kell vizsgálnia egy metódus összes visszaadható bemenetét, majd ténylegesen korlátozza a visszatérési értéket a bemenet minimális kontextusára. Ez a visszatérési érték ezután normál feldolgozáson megy keresztül.
A közvetett visszajuttatások jelentős problémát jelentenek, tekintve hogy minden ref a metódus bemenete és kimenete is. Ezek a kimenetek már ismert környezettel rendelkeznek. A fordító nem tud újakat kikövetkeztetni, azokat a jelenlegi szinten kell figyelembe vennie. Ez azt jelenti, hogy a fordítónak minden olyan ref-t meg kell vizsgálnia, amely rendelhető a hívott metódusban, értékelnie kell annak kontextusát, majd ellenőriznie kell, hogy a metódusnak nincs-e az adott -nél kisebb ref. Ha ilyen eset áll fenn, akkor a metódushívásnak illegálisnak kell lennie, mert az megsértheti ref biztonságát.
A metódusargumentumoknak egyezniük kell azzal a folyamattal, amellyel a fordító ezt a biztonsági ellenőrzést érvényesíti.
Ennek kiértékelésére a fejlesztők számára gyakran egyszerűbben használható módszer az alábbi gyakorlat elvégzése:
- Tekintse meg a metódusdefiníciót az összes olyan hely azonosításához, ahol az állapot közvetetten visszaadható: a. A módosítható
refparaméterek, amelyek aref structb-re mutatnak. Mutablerefparaméterek újrahivatkozhatórefmezőkkel c. Hozzárendelhetőrefparaméterek vagyrefmezők, amelyek aref struct-re mutatnak (vegye figyelembe rekurzívan) - Nézze meg a hívási helyet. Azonosítsa azokat a környezeteket, amelyek a b. pontban azonosított helyekkel összhangban találhatók. Azonosítsa a metódus összes olyan bemenetének kontextusát, amelyek visszaadhatók (nincsenek összhangban a
scopedparaméterekkel)
Ha a 2.b értéke kisebb, mint 2.a, akkor a metódushívásnak érvénytelennek kell lennie. Tekintsünk meg néhány példát a szabályok szemléltetésére:
ref struct R { }
class Program
{
static void F0(ref R a, scoped ref R b) => throw null;
static void F1(ref R x, scoped R y)
{
F0(ref x, ref y);
}
}
F0 hívását tekintve nézzük meg az (1) és (2) pontokat. A közvetett visszatérési potenciállal rendelkező paraméterek a és b, mivel mindkettő közvetlenül hozzárendelhető. Az argumentumok, amelyek ezekhez a paraméterekhez sorakoznak fel, a következők:
-
a, amelyxképez le -
b, amely megfelely-nek, és rendelkezik a környezet, a függvénytag-el
A metódushoz visszaadható bemenetek halmaza a következő:
-
xmenekülési-hatókörhívókörnyezet -
ref xmenekülési-hatókörhívókörnyezet -
ykilépési-hatókörfüggvénytag
A ref y érték nem visszaadható, mivel scoped ref-re képez le, ezért nem számít bemenetnek. Mivel azonban van legalább egy bemenet, amely kisebb menekülési hatókörű (y argumentum), mint az egyik kimenet (x argumentum), a függvényhívás illegális.
Egy másik változat a következő:
ref struct R { }
class Program
{
static void F0(ref R a, ref int b) => throw null;
static void F1(ref R x)
{
int y = 42;
F0(ref x, ref y);
}
}
A közvetett visszatérési potenciállal rendelkező paraméterek szintén a és b, mivel mindkettő közvetlenül hozzárendelhető. A b azonban kizárható, mert nem mutat ref struct ezért nem használható ref állapot tárolására. Így van:
-
a, amelyxképez le
A metódushoz visszaadható bemenetek készlete a következő:
-
xkörnyezettelhívókörnyezet -
ref xkörnyezettelhívókörnyezet -
ref yfüggvénytagkörnyezetével
Mivel van legalább egy bemenet kisebb érvényességi tartománnyal (ref y argumentum), mint az egyik kimenet (x argumentum), a metódushívás érvénytelen.
Ez az a logika, amelyet a metódusargumentumoknak meg kell egyeznie a szabálylal, amelyet megpróbálnak átfogni. A szöveg továbblép, mivel mind scoped-t a bemenetek megfontolásból való eltávolításának módjaként, mind readonly-t ref kimenetként való eltávolításának módjaként tekinti (nem rendelhető hozzá readonly ref, így nem lehet kimeneti forrás). Ezek a speciális esetek összetettebbé teszik a szabályokat, de ezt a fejlesztő érdekében teszik. A fordító megpróbálja eltávolítani az összes olyan bemenetet és kimenetet, amelyről tudja, hogy nem tud hozzájárulni az eredményhez, hogy a fejlesztők maximális rugalmasságot biztosítsanak egy tag meghívása során. Hasonlóan a túlterhelés feloldásához, érdemes erőfeszítéseket tenni a szabályok összetettebbé tételére, amikor nagyobb rugalmasságot biztosít a fogyasztók számára.
Példák az inferred deklarációs kifejezések biztonságos környezetére
Kapcsolódik a
ref struct RS
{
public RS(ref int x) { } // assumed to be able to capture 'x'
static void M0(RS input, out RS output) => output = input;
static void M1()
{
var i = 0;
var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M2(RS rs1)
{
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M3(RS rs1)
{
M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
}
}
Vegye figyelembe, hogy a scoped módosítóból származó helyi környezet a legszűkebb, amely esetleg használható a változóhoz – bármilyen szűkebbnek számít, azt jelentené, hogy a kifejezés olyan változókra hivatkozik, amelyek csak a kifejezésnél szűkebb kontextusban deklarálva vannak.
C# feature specifications