Delen via


Verbeterde definitieve toewijzingsanalyse

Notitie

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante LDM-notities (Language Design Meeting).

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

Probleem met kampioen: https://github.com/dotnet/csharplang/issues/4465

Samenvatting

Definitieve toewijzing §9.4 zoals gespecificeerd heeft enkele hiaten die gebruikers ongemak hebben veroorzaakt. In het bijzonder scenario's die betrekking hebben op booleaanse constanten, voorwaardelijke toegang en null-coalescenatie.

csharplang discussie over dit voorstel: https://github.com/dotnet/csharplang/discussions/4240

Waarschijnlijk zijn er een dozijn of zo veel gebruikersrapporten te vinden via deze of vergelijkbare query's (dus zoek naar 'definitieve toewijzing' in plaats van 'CS0165', of zoek in csharplang). https://github.com/dotnet/roslyn/issues?q=is%3Aclosed+is%3Aissue+label%3A%22Resolution-By+Design%22+cs0165

Ik heb verwante problemen in de onderstaande scenario's opgenomen om een beeld te geven van de relatieve impact van elk scenario.

Scenario's

Laten we als punt van referentie beginnen met een bekende "succesvolle situatie" die werkt bij bepaalde toewijzing en nullable.

#nullable enable

C c = new C();
if (c != null && c.M(out object obj0))
{
    obj0.ToString(); // ok
}

public class C
{
    public bool M(out object obj)
    {
        obj = new object();
        return true;
    }
}

Vergelijking met boolconstante

if ((c != null && c.M(out object obj1)) == true)
{
    obj1.ToString(); // undesired error
}

if ((c != null && c.M(out object obj2)) is true)
{
    obj2.ToString(); // undesired error
}

Vergelijking tussen een voorwaardelijke toegang en een constante waarde

Dit scenario is waarschijnlijk de grootste. Dit wordt wel ondersteund in nullable, maar niet in definitieve toewijzing.

if (c?.M(out object obj3) == true)
{
    obj3.ToString(); // undesired error
}

Voorwaardelijke toegang is gekoppeld aan een boolconstante

Dit scenario is vergelijkbaar met het vorige scenario. Dit wordt ook ondersteund in nullable, maar niet in definitieve toewijzing.

if (c?.M(out object obj4) ?? false)
{
    obj4.ToString(); // undesired error
}

Voorwaardelijke expressies waarbij één arm een boolconstante is

Het is de moeite waard om erop te wijzen dat we al speciaal gedrag hebben voor wanneer de voorwaardeexpressie constant is (d.w.z. true ? a : b). We bezoeken zonder voorwaarden de arm die door de constante voorwaarde wordt aangegeven en negeren de andere arm.

Houd er ook rekening mee dat we dit scenario niet hebben verwerkt in nullable.

if (c != null ? c.M(out object obj4) : false)
{
    obj4.ToString(); // undesired error
}

Specificatie

?. Uitdrukkingen met een null-voorwaardelijke operator

We introduceren een nieuwe sectie ?. Expressies (null-conditional operator). Zie de specificatie van de null-conditionele operator (§12.8.8) en de precieze regels voor het bepalen van de definitieve toewijzing (§9.4.4) voor context.

Zoals in de hierboven gekoppelde definitieve toewijzingsregels verwijzen we naar een gegeven in eerste instantie niet-toegewezen variabele als v.

We introduceren het concept «bevat direct». Een expressie E 'bevat direct' een subexpressie E1, mits deze niet onder enige door de gebruiker gedefinieerde conversie §10.5 valt waarvan de parameter niet van een niet-nullable waardetype is en een van de volgende voorwaarden geldt:

  • E is E1. a?.b() bevat bijvoorbeeld rechtstreeks de expressie a?.b().
  • Als E een tussen haakjes geplaatste uitdrukking (E2)is, en E2 rechtstreeks E1bevat.
  • Als E- een null-vergevende operatorexpressie is E2!en E2 rechtstreeks E1bevat.
  • Als E- een cast-expressie is (T)E2en de cast E2 niet onderworpen is aan een niet-gelift door de gebruiker gedefinieerde conversie waarvan de parameter niet een niet-nullbaar waardetype is, en E2 rechtstreeks E1bevat.

Voor een uitdrukking E van de vorm primary_expression null_conditional_operations, laat E0 de uitdrukking zijn die wordt verkregen door het verwijderen van de leidende "?". van elk van de null_conditional_operations van E die er een hebben, zoals in de bovenstaande gekoppelde specificatie.

In volgende secties verwijzen we naar E0 als de niet-voorwaardelijke tegenhanger van de null-conditionele expressie. Houd er rekening mee dat sommige expressies in volgende secties onderhevig zijn aan aanvullende regels die alleen van toepassing zijn wanneer een van de operanden rechtstreeks een null-voorwaardelijke expressie bevat.

  • De definitieve toewijzingsstatus van v op elk moment binnen E is hetzelfde als de definitieve toewijzingsstatus op het overeenkomstige punt binnen E0.
  • De definitieve toewijzingsstatus van v na E is hetzelfde als de definitieve toewijzingsstatus van v na primary_expression.

Opmerkingen

We gebruiken het concept 'direct bevat', waardoor we relatief eenvoudige 'wrapper'-expressies kunnen overslaan bij het analyseren van voorwaardelijke toegang die wordt vergeleken met andere waarden. Er wordt bijvoorbeeld verwacht dat ((a?.b(out x))!) == true resulteert in dezelfde stroomstatus als a?.b == true in het algemeen.

We willen ook de analyse mogelijk maken in de aanwezigheid van verschillende mogelijke conversies bij een voorwaardelijke toegang. Het doorgeven van 'status wanneer niet-null' is niet mogelijk wanneer de conversie gebruikersgedefinieerd is, omdat we niet kunnen rekenen op gebruikersgedefinieerde conversies om de beperking te respecteren dat de uitvoer alleen niet-null is als de invoer niet-null is. De enige uitzondering hierop is wanneer de invoer van de door de gebruiker gedefinieerde conversie een niet-null-waardetype is. Bijvoorbeeld:

public struct S1 { }
public struct S2 { public static implicit operator S2?(S1 s1) => null; }

Dit omvat ook verheven conversies zoals de volgende:

string x;

S1? s1 = null;
_ = s1?.M1(x = "a") ?? s1.Value.M2(x = "a");

x.ToString(); // ok

public struct S1
{
    public S1 M1(object obj) => this;
    public S2 M2(object obj) => new S2();
}
public struct S2
{
    public static implicit operator S2(S1 s1) => default;
}

Wanneer we overwegen of een variabele is toegewezen op een bepaald punt binnen een null-voorwaardelijke expressie, wordt ervan uitgegaan dat alle voorgaande null-voorwaardelijke bewerkingen binnen dezelfde null-voorwaardelijke expressie zijn geslaagd.

Neem bijvoorbeeld een voorwaardelijke expressie a?.b(out x)?.c(x), dan is de niet-voorwaardelijke tegenhanger a.b(out x).c(x). Als we de definitieve toewijzingsstatus van x vóór ?.c(x)willen weten, voeren we bijvoorbeeld een 'hypothetische' analyse van a.b(out x) uit en gebruiken we de resulterende status als invoer voor ?.c(x).

Booleaanse constante expressies

We introduceren een nieuwe sectie 'Booleaanse constante expressies':

Voor een expressie expr waarbij expr- een constante expressie is met een boolwaarde:

  • De definitieve toewijzingsstatus van v na expr- wordt bepaald door:
    • Als expr een constante uitdrukking is met de waarde waar, en de status van v voordat expr 'niet zeker toegewezen' is, dan is de status van v na expr 'zeker toegewezen wanneer vals'.
    • Als expr een constante expressie is met de waarde onwaar, en de toestand van v vóór expr 'niet zeker toegewezen' is, dan is de toestand van v na expr 'zeker toegewezen als waar'.

Opmerkingen

We gaan ervan uit dat als een expressie een constante waarde bool falseheeft, het bijvoorbeeld onmogelijk is om een vertakking te bereiken waarvoor de expressie moet worden geretourneerd true. Daarom wordt ervan uitgegaan dat variabelen in dergelijke vertakkingen definitief worden toegewezen. Dit sluit uiteindelijk mooi aan bij de specificatiewijzigingen voor uitdrukkingen zoals ?? en ?: en maakt veel nuttige scenario's mogelijk.

Het is ook de moeite waard om te vermelden dat we nooit een voorwaardelijke status hebben voordat een constante expressie bezoekt. Daarom maken we geen rekening met scenario's zoals "expr is een constante expressie met waarde waar, en de status van v voordat expr is 'zeker toegewezen als waar'.

?? (null-coalescing-expressies) vergroten

We verbeteren sectie §9.4.4.29 als volgt:

Voor een expressie expr van het formulier expr_first ?? expr_second:

  • ...
  • De definitieve toewijzingsstatus van v na expr- wordt bepaald door:
    • ...
    • Als expr_first rechtstreeks een null-conditionele expressie Ebevat, en v definitief wordt toegewezen na de niet-voorwaardelijke tegenhanger E0, is de definitieve toewijzingsstatus van v na expr hetzelfde als de definitieve toewijzingsstatus van v na expr_second.

Opmerkingen

De bovenstaande regel formaliseert dat voor een expressie zoals a?.M(out x) ?? (x = false)de a?.M(out x) volledig is geëvalueerd en een niet-null-waarde is geproduceerd, in welk geval x is toegewezen, of dat de x = false is geëvalueerd, in welk geval x ook is toegewezen. Daarom wordt x na deze expressie altijd toegewezen.

Dit behandelt ook het dict?.TryGetValue(key, out var value) ?? false-scenario, door te observeren dat v zeker is toegewezen na dict.TryGetValue(key, out var value), en dat v 'zeker toegewezen wanneer waar' is na false, en te concluderen dat v 'zeker toegewezen wanneer waar' moet zijn.

De meer algemene formulering stelt ons ook in staat om een aantal meer ongebruikelijke scenario's af te handelen, zoals:

  • if (x?.M(out y) ?? (b && z.M(out y))) y.ToString();
  • if (x?.M(out y) ?? z?.M(out y) ?? false) y.ToString();

?: (voorwaardelijke) expressies

We verbeteren sectie §9.4.4.30 als volgt:

Voor een expressie expr van het formulier expr_cond ? expr_true : expr_false:

  • ...
  • De definitieve toewijzingsstatus van v na expr- wordt bepaald door:
    • ...
    • Als de status van v na expr_true 'zeker toegewezen wanneer waar' is en de status van v na expr_false 'zeker toegewezen wanneer waar' is, is de status van v na expr 'zeker toegewezen wanneer waar'.
    • Als de status van v na expr_true 'zeker toegewezen wanneer onwaar' is, en de status van v na expr_false 'zeker toegewezen wanneer onwaar' is, dan wordt de status van v na expr 'zeker toegewezen wanneer onwaar'.

Opmerkingen

Dit zorgt ervoor dat wanneer beide armen van een voorwaardelijke expressie resulteren in een voorwaardelijke status, we de bijbehorende voorwaardelijke statussen samenvoegen en doorgeven in plaats van de status te splitsen en de uiteindelijke status toe te staan niet-voorwaardelijk te zijn. Dit maakt scenario's mogelijk, zoals de volgende:

bool b = true;
object x = null;
int y;
if (b ? x != null && Set(out y) : x != null && Set(out y))
{
  y.ToString();
}

bool Set(out int x) { x = 0; return true; }

Dit is een niche scenario dat zonder fouten in de systeemeigen compiler compileert, maar in Roslyn werd aangepast om te voldoen aan de toenmalige specificatie.

==/!= (relationele gelijkheidsoperator) expressies

We introduceren een nieuwe sectie ==/!= (relationele gelijkheidsoperator) expressies.

De algemene regels voor expressies met ingebedde expressies §9.4.4.23 zijn van toepassing, met uitzondering van de onderstaande scenario's.

Voor een expressie expr van het formulier expr_first == expr_second, waarbij == een vooraf gedefinieerde vergelijkingsoperator is (§12.12) of een opgeheven operator (§12.4.8), wordt de definitieve toewijzingsstatus van v na expr bepaald door:

  • Als expr_first rechtstreeks een null-voorwaardelijke expressie bevat E- en expr_second een constante expressie is met de waarde null-, en de status van v na de niet-voorwaardelijke tegenhanger E0 is 'zeker toegewezen', dan is de status van v na expr 'zeker toegewezen wanneer onwaar'.
  • Als expr_first rechtstreeks een null-voorwaardelijke expressie E bevat en expr_second een expressie is van een niet-null-waardetype, of een constante expressie met een niet-null-waarde, en de status van v na de niet-voorwaardelijke tegenhanger E0 "definitief toegewezen" is, dan is de status van v na expr "definitief toegewezen wanneer waar".
  • Als expr_first van het type booleanis, en expr_second een constante expressie is met waarde waar, is de definitieve toewijzingsstatus na expr hetzelfde als de definitieve toewijzingsstatus na expr_first.
  • Als expr_first van het type booleaanseis en expr_second een constante expressie is met waarde onwaar, is de definitieve toewijzingsstatus na expr- hetzelfde als de definitieve toewijzingsstatus van v na de logische negatie-expressie !expr_first.

Voor een expressie expr van het formulier expr_first != expr_second, waarbij != een vooraf gedefinieerde vergelijkingsoperator is (§12.12) of een lifted operator ((§12.4.8)), wordt de definitieve toewijzingsstatus van v na expr- bepaald door:

  • Als expr_first rechtstreeks een nul-voorwaardelijke uitdrukking E bevat en expr_second een constante uitdrukking is met waarde nullen de status van v na de niet-voorwaardelijke tegenhanger E0 'zeker toegewezen' is, dan is de status van v na expr 'zeker toegewezen wanneer waar'.
  • Als expr_first direct een null-voorwaardelijke expressie E bevat en expr_second een expressie is van een niet-null-waardetype, of een constante expressie met een niet-null-waarde, en de status van v na de niet-voorwaardelijke tegenhanger E0 'definitief toegewezen' is, dan is de status van v na expr 'definitief toegewezen als onwaar'.
  • Als expr_first van het type booleanis en expr_second een constante expressie is met waarde waar, dan is de definitieve toewijzingsstatus na expr hetzelfde als de definitieve toewijzingsstatus van v na de logische negatieexpressie !expr_first.
  • Als expr_first van het type Booleaanseis en expr_second een constante expressie is met waarde onwaar, is de definitieve toewijzingsstatus na expr hetzelfde is als de definitieve toewijzingsstatus na expr_first.

Alle bovenstaande regels in deze sectie zijn commutatief, wat betekent dat als een regel van toepassing is wanneer deze wordt geëvalueerd in het formulier expr_second op expr_first, dit ook van toepassing is in de vorm expr_first op expr_second.

Opmerkingen

Het algemene idee dat door deze regels wordt uitgedrukt, is:

  • Als een voorwaardelijke toegangstoegang wordt vergeleken met null, weten we dat de bewerkingen zeker hebben plaatsgevonden als het resultaat van de vergelijking false is.
  • als een voorwaardelijke toegang wordt vergeleken met een niet-null-waardetype of een niet-null-constante, weten we dat de bewerkingen zeker hebben plaatsgevonden als het resultaat van de vergelijking trueis.
  • omdat we door de gebruiker gedefinieerde operators niet kunnen vertrouwen om betrouwbare antwoorden te bieden op het gebied van initialisatieveiligheid, zijn de nieuwe regels alleen van toepassing wanneer een vooraf gedefinieerde ==/!= operator wordt gebruikt.

Uiteindelijk willen we deze regels verfijnen om ze door te voeren in de voorwaardelijke toestand die aan het einde van een lidtoegang of -aanroep aanwezig is. Dergelijke scenario's gebeuren niet echt in definitieve toewijzing, maar ze gebeuren wel in nullable in aanwezigheid van [NotNullWhen(true)] en vergelijkbare kenmerken. Dit vereist speciale verwerking voor bool constanten, naast alleen verwerking voor null/niet-null-constanten.

Enkele gevolgen van deze regels:

  • if (a?.b(out var x) == true)) x() else x(); zal een fout geven in de 'else'-vertakking.
  • if (a?.b(out var x) == 42)) x() else x(); zal een fout geven in de 'else' vertakking
  • if (a?.b(out var x) == false)) x() else x(); zal een fout geven in de 'else'-vertakking
  • if (a?.b(out var x) == null)) x() else x(); zal een fout veroorzaken in de 'then' vertakking
  • if (a?.b(out var x) != true)) x() else x(); geeft een fout in de 'then'-vertakking
  • if (a?.b(out var x) != 42)) x() else x(); wordt in de vertakking 'then' fout weergegeven
  • if (a?.b(out var x) != false)) x() else x(); zal een fout geven in de 'then' tak
  • if (a?.b(out var x) != null)) x() else x(); zal een fout opleveren in de 'else'-vertakking

is-operator en is-patroonexpressies

We introduceren een nieuwe sectie is operator en is patroonexpressies.

Voor een expressie expr van het formulier E is T, waarbij T- elk type of patroon is

  • De definitieve toewijzingsstatus van v voordat E hetzelfde is als de definitieve toewijzingsstatus van v voordat expr.
  • De definitieve toewijzingsstatus van v na expr- wordt bepaald door:
    • Als E- rechtstreeks een null-voorwaardelijke uitdrukking bevat, en de status van v na de niet-voorwaardelijke tegenhanger E0 is 'definitief toegewezen', en T is elk type of een patroon dat niet overeenkomt met een null invoer, dan is de status van v na expr 'definitief toegewezen wanneer deze waar is'.
    • Als E direct een null-voorwaardelijke expressie bevat en de toestand van v na de niet-voorwaardelijke tegenhanger E0 'definitief toegewezen' is, en T een patroon is dat overeenkomt met een null invoer, dan is de toestand van v na expr 'definitief toegewezen als het onwaar is'.
    • Als E van het type Booleaanse waarde is en T een patroon is dat alleen overeenkomt met een true invoer, is de definitieve toewijzingsstatus van v na expr hetzelfde is als de definitieve toewijzingsstatus van v na E.
    • Als E- van het type Booleaanse waarde is en T een patroon is dat alleen overeenkomt met een false invoer, is de definitieve toewijzingsstatus van v nadat expr hetzelfde is als de definitieve toewijzingsstatus van v na de logische negatie-expressie !expr.
    • Anders, als de definitieve toewijzingsstatus van v na E 'zeker toegewezen' is, dan is de definitieve toewijzingsstatus van v na expr ook 'zeker toegewezen'.

Opmerkingen

Deze sectie is bedoeld om vergelijkbare scenario's aan te pakken, zoals in de bovenstaande sectie ==/!=. Deze specificatie heeft geen betrekking op recursieve patronen, bijvoorbeeld (a?.b(out x), c?.d(out y)) is (object, object). Dergelijke ondersteuning kan later komen als de tijd het toelaat.

Aanvullende scenario's

Deze specificatie heeft momenteel geen betrekking op scenario's met betrekking tot expressies voor patroonwisselingen en switchinstructies. Bijvoorbeeld:

_ = c?.M(out object obj4) switch
{
    not null => obj4.ToString() // undesired error
};

Het lijkt erop dat de ondersteuning hiervoor later zou kunnen komen als de tijd het toelaat.

Er zijn verschillende categorieën bugs opgeslagen voor nullable, waarvoor we in wezen de verfijning van patroonanalyse moeten verhogen. Het is waarschijnlijk dat elke uitspraak die we maken die de definitieve toewijzing verbetert, ook zou worden overgedragen naar nullable.

https://github.com/dotnet/roslyn/issues/49353
https://github.com/dotnet/roslyn/issues/46819
https://github.com/dotnet/roslyn/issues/44127

Nadelen

Het voelt vreemd om de analyse 'naar beneden te halen' en speciale erkenning van voorwaardelijke toegang te hebben, wanneer de status van de stroomanalyse normaal gesproken omhoog moet worden doorgegeven. We zijn bezorgd over hoe een oplossing zoals deze pijnlijk kan botsen met toekomstige taalfuncties die null-controles uitvoeren.

Alternatieven

Twee alternatieven voor dit voorstel:

  1. Voeg 'state when null' en 'state when not null' toe aan de programmeertaal en de compiler. Dit is beoordeeld als te veel moeite voor de scenario's die we proberen op te lossen, maar dat we eventueel het bovenstaande voorstel kunnen implementeren en vervolgens later kunnen overstappen naar een model met de status 'wanneer null/niet null' zonder problemen voor mensen te veroorzaken.
  2. Doe niets.

Niet-opgeloste vragen

Er zijn gevolgen voor switchexpressies die moeten worden opgegeven: https://github.com/dotnet/csharplang/discussions/4240#discussioncomment-343395

Ontwerpvergaderingen

https://github.com/dotnet/csharplang/discussions/4243