Partager via


Syndicats

Note

Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Elle inclut les changements de spécification proposés, ainsi que les informations nécessaires à la conception et au développement de la fonctionnalité. Ces articles sont publiés jusqu'à ce que les changements proposés soient finalisés et incorporés dans la spécification ECMA actuelle.

Il peut y avoir des divergences entre la spécification de la fonctionnalité et l'implémentation réalisée. Ces différences sont capturées dans les notes de réunion de conception de langage (LDM) pertinentes .

Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications.

Problème de champion : https://github.com/dotnet/csharplang/issues/9662

Résumé

Les unions sont un ensemble de fonctionnalités entrelacées, qui combinent pour fournir une prise en charge C# pour les types d’union :

  • Types d’union : les structs et les classes qui ont un [Union] attribut sont reconnus comme des types d’union et prennent en charge les comportements d’union.
  • Types de cas : les types Union ont un ensemble de types de cas, qui sont donnés par des paramètres aux constructeurs et aux méthodes de fabrique.
  • Comportements d’union : les types d’union prennent en charge les comportements d’union suivants :
    • Conversions d’union : il existe des conversions d’union implicites de chaque type de cas en type union.
    • Correspondance de l’union : modèle correspondant à des valeurs d’union implicitement « désencapsuler » leur contenu, en appliquant le modèle à la valeur sous-jacente à la place.
    • Exhaustive de l’union : basculer les expressions sur les valeurs d’union sont exhaustives lorsque tous les types de cas ont été mis en correspondance, sans avoir besoin d’un cas de secours.
    • Possibilité d’union nullabilité : l’analyse de la nullabilité a amélioré le suivi de l’état Null du contenu d’une union.
  • Modèles d’union : tous les types d’union suivent un modèle d’union de base, mais il existe des modèles facultatifs supplémentaires pour des scénarios spécifiques.
  • Déclarations d’union : une syntaxe abrégée autorise directement la déclaration des types union. L’implémentation est « avisée » : déclaration de struct qui suit le modèle d’union de base et stocke le contenu en tant que champ de référence unique.
  • Interfaces union : quelques interfaces sont connues par le langage et utilisées dans son implémentation de déclarations d’union.

Motivation

Les unions sont une fonctionnalité C# longue demande, qui permet d’exprimer des valeurs à partir d’un ensemble fermé de types d’une manière que la mise en correspondance de modèle peut approuver pour être exhaustive.

La séparation entre les types d’union et les déclarations d’union permet à C# d’avoir une syntaxe de déclaration d’union succincte avec une sémantique opinionée, tout en permettant également aux types ou types existants avec d’autres choix d’implémentation d’opter pour les comportements d’union.

Les syndicats proposés en C# sont des syndicats de types et non « discriminés » ou « marqués ». Les « unions discriminatoires » peuvent être exprimées en termes de « unions de type » à l’aide de déclarations de type fraîches en tant que types de cas. Elles peuvent également être implémentées en tant que hiérarchie fermée, qui est une autre fonctionnalité C# connexe et à venir axée sur l’exhaustive.

Conception détaillée

Types union

Tout type de classe ou de struct avec un System.Runtime.CompilerServices.UnionAttribute attribut est considéré comme un type union :

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(Class | Struct, AllowMultiple = false)]
    public class UnionAttribute : Attribute;
}

Un type d’union doit suivre un certain modèle de membres de l’union publique, qui doit être déclaré sur le type d’union lui-même ou délégué à un « fournisseur de membres de l’union ».

Certains membres de l’union sont obligatoires et d’autres sont facultatifs.

Un type union a un ensemble de types de cas qui sont établis en fonction des signatures de certains membres de l’union.

Le contenu d’une valeur d’union est accessible via une Value propriété. La langue part du principe que Value seule une valeur d’un des types de cas ou null (voir Well-formness).

Fournisseurs membres de l’union

Par défaut, les membres de l’union se trouvent sur le type d’union lui-même. Toutefois, si le type d’union contient directement une déclaration d’une interface appelée IUnionMembers , l’interface agit en tant que fournisseur de membres de l’union. Dans ce cas, les membres de l’union ne sont trouvés que sur le fournisseur de membres de l’union, et non sur le type d’union lui-même.

Une interface de fournisseur de membre d’union doit être publique et le type d’union lui-même doit l’implémenter en tant qu’interface.

Nous utilisons le terme type de définition d’union pour le type où les membres de l’union sont trouvés : le fournisseur de membres de l’union s’il existe et le type d’union lui-même dans le cas contraire.

Membres de l’union

Les membres de l’union sont recherchés par nom et signature sur le type de définition de l’union. Ils ne doivent pas être déclarés directement sur le type de définition d’union, mais peuvent être hérités.

Il s’agit d’une erreur pour tout membre du syndicat qui ne doit pas être public.

Les membres de création et la Value propriété sont obligatoires et sont collectivement appelés modèle d’union de base.

Les HasValue membres et TryGetValue les membres sont collectivement appelés modèle d’accès non boxing union.

Les différents membres de l’union sont décrits dans les sections suivantes.

Membres de création d’union

Les membres de création d’union sont utilisés pour créer de nouvelles valeurs d’union à partir d’une valeur de type casse.

Si le type de définition d’union est le type union lui-même, chaque constructeur avec un seul paramètre est un constructeur d’union. Les types de cas de l’union sont identifiés comme l’ensemble de types générés à partir de types de paramètres de ces constructeurs de la manière suivante :

  • Si le type de paramètre est un type Nullable (valeur ou référence), le type de cas est le type sous-jacent
  • Sinon, le type de cas est le type de paramètre.
// Union constructor making `Dog` a case type
public Pet(Dog value) { ... }
// Union constructor making `int` a case type
public Union(int? value) { ... }
// Union constructor making `string` a case type
public Union(string? value) { ... }

Si le type de définition d’union est un fournisseur de membres d’union, chaque méthode statique Create avec un paramètre unique et un type de retour qui est identity-convertible en type union lui-même est une méthode union factory. Les types de cas de l’union sont identifiés comme l’ensemble de types générés à partir de types de paramètres de ces méthodes de fabrique de la manière suivante :

  • Si le type de paramètre est un type Nullable (valeur ou référence), le type de cas est le type sous-jacent
  • Sinon, le type de cas est le type de paramètre.
// Union factory method making `Cat` a case type
public static Pet Create(Cat value) { ... }
// Union factory method making `int` a case type
public static Union Create(int? value) { ... }
// Union factory method making `string` a case type
public static Union Create(string? value) { ... }

Les constructeurs d’union et les méthodes d’usine d’union sont appelés collectivement des membres de création d’union.

Le paramètre unique d’un membre de création d’union doit être un paramètre par valeur ou in paramètre.

Un type d’union doit avoir au moins un membre de création d’union, et par conséquent au moins un type de cas.

Value, propriété

La Value propriété autorise l’accès à la valeur contenue dans une union, quel que soit son type de cas.

Chaque type de définition d’union doit déclarer une Value propriété de type object? ou object. La propriété doit avoir un get accesseur et peut éventuellement avoir un ou set un init accesseur, qui peut être de toute accessibilité et n’est pas utilisé par le compilateur.

// Union 'Value' property
public object? Value { get; }

Membres d’accès non boxing

Un type d’union peut choisir d’implémenter en outre le modèle d’accès non boxing union, qui permet un accès conditionnel fortement typé à chaque type de cas, ainsi qu’un moyen de vérifier la valeur Null.

Cela permet au compilateur d’implémenter la mise en correspondance des modèles plus efficacement lorsque les types de cas sont des types valeur et stockés en tant que tels au sein de l’union.

Les membres d’accès non boxing sont les suivants :

  • Propriété HasValue de type bool avec un accesseur public get . Il peut éventuellement avoir un ou set un init accesseur, qui peut être de toute accessibilité et n’est pas utilisé par le compilateur.
  • Méthode TryGetValue pour chaque type de cas. La méthode retourne bool et prend un seul paramètre out d’un type qui est identity-convertible en type case.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }

HasValue est censé retourner true si et seulement si l’union n’est Value pas null.

TryGetValue est censé retourner true si et uniquement si l’union Value est du type de cas donné, et si c’est le cas, fournissez cette valeur dans le paramètre out de la méthode.

Bien-formé

Le langage et le compilateur effectuent un certain nombre d’hypothèses comportementales sur les types d’union. Si un type se qualifie comme un type d’union mais ne répond pas à ces hypothèses, les comportements d’union peuvent ne pas fonctionner comme prévu.

  • Sonness : la Value propriété prend toujours la valeur Null ou une valeur d’un type case. Cela est vrai même pour la valeur par défaut du type union.
  • Stabilité : si une valeur union est créée à partir d’un type de cas, la Value propriété correspond à ce type de cas ou null. Si une valeur d’union est créée à partir d’une null valeur, la Value propriété est null.
  • Équivalence de la création : si une valeur est implicitement convertible en deux types de cas différents, le membre de création de l’un de ces types de cas a le même comportement observable lorsqu’il est appelé avec cette valeur.
  • Cohérence du modèle d’accès : le comportement des membres d’accès HasValue non TryGetValue boxing, le cas échéant, équivaut de façon observable à celle de la vérification directe de la Value propriété.

Exemples de types union

Pet implémente le modèle d’union de base sur le type d’union lui-même :

[Union] public record struct Pet
{
    // Creation members = case types are 'Dog' and 'Cat'
    public Pet(Dog value) => Value = value;
    public Pet(Cat value) => Value = value;

    // 'Value' property
    public object? Value { get; }
}

IntOrBool implémente le modèle d’accès non boxing sur le type d’union lui-même :

public record struct IntOrBool
{
    private bool _isBool;
    private int _value;

    public IntOrBool(int value) => (_isBool, _value) = (false, value);
    public IntOrBool(bool value) => (_isBool, _value) = (true, value ? 1 : 0);

    public object Value => _isBool ? _value is 1 : _value;

    public bool HasValue => true;
    public bool TryGetValue(out int value)
    {
        value = _value;
        return !_isBool;
    }
    public bool TryGetValue(out bool value)
    {
        value = _isBool && _value is 1;
        return _isBool;
    }
}

Note: Il s’agit simplement d’un exemple de la façon dont le modèle d’accès non boxing peut être implémenté. Le code utilisateur peut stocker le contenu comme il le souhaite. En particulier, il n’empêche pas l’implémentation de boxer ! Son non-boxing nom fait référence à l’autorisation de l’implémentation de correspondance de modèle du compilateur pour accéder à chaque type de cas d’une manière fortement typée, par opposition à la object?propriété -typée Value .

Result<T> implémente le modèle de base via un fournisseur de membres d’union :

public record class Result<T> : Result<T>.IUnionMembers
{
    object? _value;

    public interface IUnionMembers
    {
        public static Result<T> Create(T value) => new() { _value = value };
        public static Result<T> Create(Exception value) => new() { _value = value };

        public object? Value { get; }
    }

    object? IUnionMembers.Value => _value;
}

Comportements de l’union

Les comportements d’union sont généralement implémentés par le biais du modèle d’union de base. Si l’union offre le modèle d’accès non boxing, la mise en correspondance des modèles d’union l’utilisera de manière privilégiée.

Conversions d’union

Une conversion d’union convertit implicitement en un type union à partir de chacun de ses types de cas. Plus précisément, il existe une conversion union vers un type U union à partir d’un type ou d’une expression E s’il existe une conversion implicite standard d’un E type C et C est un type de paramètre d’un membre de création d’union de U. Si le type U union est un struct, il existe une conversion union en type U? à partir d’un type ou d’une expression E s’il existe une conversion implicite standard d’un E type C et C est un type de paramètre d’un membre de création d’union de U.

Une conversion d’union n’est pas elle-même une conversion implicite standard. Il peut donc ne pas participer à une conversion implicite définie par l’utilisateur ou à une autre conversion union.

Il n’existe aucune conversion d’union explicite au-delà des conversions implicites d’union. Par conséquent, même s’il existe une conversion explicite du E type Cde cas d’une union, cela ne signifie pas qu’il existe une conversion explicite de E ce type d’union.

Une conversion d’union est exécutée en appelant le membre de création de l’union :

Pet pet = dog;
// becomes
Pet pet = new Pet(dog);
// and
Result<string> result = "Hello"
//becomes
Result<string> result = Result<string>.IUnionMembers.Create("Hello");

Il s’agit d’une erreur si la résolution de surcharge ne trouve pas de membre candidat unique ou si ce membre n’est pas l’un des membres du syndicat du type union.

La conversion union n’est qu’une autre forme de conversion implicite définie par l’utilisateur. Conversion d’union « ombres » définie par l’utilisateur applicable.

La justification de cette décision :

Si quelqu’un a écrit un opérateur défini par l’utilisateur, il doit obtenir la priorité. En d’autres termes, si l’utilisateur a écrit son propre opérateur, il veut que nous l’appelons. Les types existants avec des opérateurs de conversion transformés en types union continuent de fonctionner de la même façon que le code existant utilisant les opérateurs aujourd’hui.

Dans l’exemple suivant, une conversion implicite définie par l’utilisateur prend la priorité sur une conversion d’union.

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => ...
    public S1(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static implicit operator S1(int x) => ...
}

class Program
{
    static S1 Test1() => 10; // implicit operator S1(int x) is used
    static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
}

Dans l’exemple suivant, lorsque le cast explicite est utilisé dans le code, une conversion explicite définie par l’utilisateur prend la priorité sur une conversion d’union. Toutefois, lorsqu’il n’existe aucun cast explicite dans le code, une conversion union est utilisée, car la conversion explicite définie par l’utilisateur n’est pas applicable.

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => ...
    public S2(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static explicit operator S2(int x) => ...
}

class Program
{
    static S2 Test3() => 10; // Union conversion S2.S2(int) is used
    static S2 Test4() => (S2)20; // explicit operator S2(int x)
}

Correspondance de l’union

Lorsque la valeur entrante d’un modèle est d’un type union ou d’une valeur Nullable d’un type union, la valeur nullable et le contenu de la valeur d’union sous-jacente peuvent être « décompressés », selon le modèle.

Pour les modèles et var les conditions inconditionnelles_, le modèle est appliqué à la valeur entrante elle-même. Par exemple:

if (GetPet() is var pet) { ... } // 'pet' is the union value returned from `GetPet`

Toutefois, tous les autres modèles sont implicitement appliqués à la propriété de l’union Value sous-jacente :

if (GetPet() is Dog dog) { ... }   // 'Dog dog' is applied to 'GetPet().Value'
if (GetPet() is null) { ... }      // 'null' is applied to 'GetPet().Value'
if (GetPet() is { } value) { ... } // '{ } value' is applied to 'GetPet().Value'

Pour les modèles logiques, cette règle est appliquée individuellement aux branches, en tenant compte du fait que la branche gauche d’un and modèle peut affecter le type entrant de la branche droite :

GetPet() switch
{
    var pet and not null   => ... // 'var pet' applies to the incoming 'Pet' and 'not null' to its 'Value'
    not null and var value => ... // 'not null' applies to the 'Value' as does 'var value' because of the 
                                  // left branch changing the incoming type to `object?`.
}

Note: Cette règle signifie qu’elle GetPet() is Pet pet ne réussira probablement pas, comme Pet elle est appliquée au contenu, et non à l’union Pet elle-même.

Note: La raison du traitement différent du modèle inconditionnel var (ainsi que _, qui est essentiellement un raccourci pour var _) est une hypothèse que leur utilisation est qualitativement différente des autres modèles. var les modèles sont utilisés simplement pour nommer la valeur mise en correspondance, souvent dans les modèles imbriqués, tels que PetOwner{ Pet: var pet }. Ici, la sémantique utile est de pet conserver le type Petd’union, au lieu de la Value propriété en cours de déréférencement à un type inutile object? .

Si la valeur entrante est un type de classe, le null modèle réussit, qu’il s’agisse de la valeur d’union elle-même null ou de sa valeur nullcontenue :

if (result is null) { ... } // if (result == null || result.Value == null)

Les autres modèles de correspondance d’union réussissent uniquement lorsque la valeur de l’union elle-même n’est pas null.

if (result is 1) { ... } // if (result != null && result.Value is 1)

De même, si la valeur entrante est un type de valeurs nullables (encapsulant un type d’union de struct), le null modèle réussit, que la valeur entrante elle-même soit null ou sa valeur contenue soit null:

if (result is null) { ... } // if (result.HasValue == false || result.GetValueOrDefault().Value == null)

Les autres modèles de correspondance d’union réussissent uniquement lorsque la valeur entrante elle-même n’est pas null.

if (result is 1) { ... } // if (result.HasValue && result.GetValueOrDefault().Value is 1)

Le compilateur préfère implémenter le comportement du modèle par le biais de membres prescrits par le modèle d’accès non boxing. Bien qu’il soit libre d’effectuer une optimisation dans les limites des règles de bonne forme, voici l’ensemble minimal garanti d’application :

  • Pour un modèle qui implique la vérification d’un type Tspécifique, si une TryGetValue(S value) méthode est disponible et qu’il existe une identité, ou une conversion de référence/boxing implicite de T vers S, cette méthode est utilisée pour obtenir la valeur. Le modèle est ensuite appliqué à cette valeur. S’il existe plusieurs méthodes de ce type, toute conversion à partir de T laquelle S la conversion n’est pas une conversion de boxing est préférée si disponible. S’il existe encore plusieurs méthodes, une méthode est choisie de manière définie par l’implémentation.
  • Sinon, pour un modèle qui implique la vérification null, si une HasValue propriété est disponible, cette propriété est utilisée pour vérifier si la valeur union est null.
  • Sinon, le modèle est appliqué au résultat de l’accès à la IUnion.Value propriété sur l’union entrante.

L’opérateur is-type appliqué à un type union a la même signification qu’un modèle de type appliqué au type union.

Exhaustive de l’union

Un type union est supposé être « épuisé » par ses types de cas. Cela signifie qu’une switch expression est exhaustive si elle gère tous les types de cas d’une union :

var name = pet switch
{
    Dog dog => ...,
    Cat cat => ...,
    // No warning about non-exhaustive switch
};

Nullabilité

L’état Null de la Value propriété d’une union est suivi comme n’importe quelle autre propriété, avec ces modifications :

  • Lorsqu’un membre de création d’union est appelé (explicitement ou par le biais d’une conversion d’union), le nouvel union obtient l’état Value Null de la valeur entrante.
  • Lorsque le modèle d’accès non boxing ou HasValueTryGetValue(...) est utilisé pour interroger le contenu d’un type union (explicitement ou via une correspondance de modèle), il affecte Valuel’état de nullabilité de la même façon que s’il Value avait été vérifié directement : l’état null de Value devient « non null » sur la true branche.

Même lorsqu’un commutateur d’union est sinon exhaustif, si l’état null de la propriété de l’union Value entrante est « peut-être null », un avertissement est donné sur une valeur Null non gérée.

Pet pet = GetNullableDog(); // 'pet.Value' is "maybe null"
var value = pet switch
{
    Dog dog => ...,
    Cat cat => ...,
    // Warning: 'null' not handled
}

Interfaces union

Les interfaces suivantes sont utilisées par le langage dans son implémentation des fonctionnalités union.

Interface d’accès union

L’interface IUnion marque un type comme type union au moment de la compilation et permet d’accéder au contenu de l’union au moment de l’exécution.

public interface IUnion
{
    // The value of the union or null
    object? Value { get; }
}

Les unions générées par le compilateur implémentent cette interface.

Exemple d’utilisation :

if (value is IUnion { Value: null }) { ... }

Déclarations d’union

Les déclarations d’union sont un moyen succinct et opinionné de déclarer des types d’union en C#. Ils déclarent un struct qui utilise une référence d’objet unique pour stocker son Value, ce qui signifie :

  • Boxe : tous les types valeur parmi leurs types de cas seront boxés lors de l’entrée.
  • Compactage : les valeurs d’union contiennent uniquement un seul champ.

L’intention est que les déclarations syndicales couvrent très bien la grande majorité des cas d’usage. Les deux principales raisons de coder manuellement des types d’union spécifiques plutôt que d’utiliser des déclarations union sont censées être :

  • Adaptation des types existants aux modèles d’union pour obtenir des comportements d’union.
  • Implémentation d’une stratégie de stockage différente pour des raisons d’efficacité ou d’interopérabilité.

Syntaxe

Une déclaration d’union a un nom et une liste de types de constructeurs union .

union_declaration
    : attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list?
      '(' type (',' type)* ')'  struct_interfaces? type_parameter_constraints_clause* 
      (`{` struct_member_declaration* `}` | ';')
    ;

Outre les restrictions relatives aux membres du struct (§16.3), les éléments suivants s’appliquent aux membres de l’union :

  • Les champs d’instance, les propriétés automatiques ou les événements de type champ ne sont pas autorisés.
  • Les constructeurs publics déclarés explicitement avec un seul paramètre ne sont pas autorisés.
  • Les constructeurs explicitement déclarés doivent utiliser un this(...) initialiseur pour (directement ou indirectement) déléguer à l’un des constructeurs générés.

Les types de constructeurs d’union peuvent être n’importe quel type qui se convertit en object, par exemple, des interfaces, des paramètres de type, des types nullables et d’autres unions. Il est correct que les cas résultants se chevauchent et que les unions imbriquez ou soient null.

Exemples :

// Union of existing types
public union Pet(Cat, Dog, Bird);

// Union with function member
public union OneOrMore<T>(T, IEnumerable<T>)
{
    public IEnumerable<T> AsEnumerable() => Value switch
    {
        IEnumerable<T> list => list,
        T value => [value],
    }
}

// "Discriminated" union with freshly declared case types
public record class None();
public record class Some<T>(T value);
public union Option<T>(None, Some<T>);

#### Lowering

A union declaration is lowered to a struct declaration with

* the same attributes, modifiers, name, type parameters and constraints,
* implicit implementations of `IUnion`,
* a `public object? Value { get; }` auto-property,
* a public constructor for each *union constructor* type,
* any members in the union declaration's body.

It is an error for user-declared members to conflict with generated members.

Example:

``` c#
public union Pet(Cat, Dog){ ... }

Est réduit à :

[Union] public struct Pet : IUnion
{
    public Pet(Cat value) => Value = value;
    public Pet(Dog value) => Value = value;
    
    public object? Value { get; }
    
    ... // original body
}

Questions ouvertes

[Résolu] La déclaration d’union est-elle un enregistrement ?

Une déclaration d’union est réduite à un struct d’enregistrement

Je pense que ce comportement par défaut est inutile et, étant donné qu’il n’est pas configurable, va limiter considérablement les scénarios d’utilisation. Les enregistrements génèrent un grand nombre de code inutilisés ou ne correspondent pas à des exigences spécifiques. Par exemple, les enregistrements sont pratiquement interdits dans la base de code du compilateur en raison de ce ballonnement de code. Je pense qu’il serait préférable de modifier la valeur par défaut :

  • Par défaut, une déclaration d’union déclare un struct normal avec uniquement des membres spécifiques à l’union.
  • Un utilisateur peut déclarer une union d’enregistrements : record union U(E1, ...) ...

Résolution: Une déclaration d’union est un struct simple, et non un struct d’enregistrement. Le record union ... fichier n’est pas pris en charge

[Résolu] Syntaxe de déclaration union

Il semble que la syntaxe proposée soit incomplète ou inutilement limitée. Par exemple, il semble que la clause de base n’est pas autorisée. Toutefois, je peux facilement imaginer un besoin d’implémenter une interface, par exemple. Je pense qu’en dehors de la liste des types d’éléments, la syntaxe doit correspondre à la déclaration régulière struct/record struct où le mot clé est remplacé par union le struct mot clé.

Résolution: La restriction est supprimée.

[Résolu] Membres de la déclaration de l’union

Les champs d’instance, les propriétés automatiques ou les événements de type champ ne sont pas autorisés.

Cela se sent arbitraire et absolument inutile.

Résolution: La restriction est conservée.

[Résolu] Types valeur nullables en tant que types de cas Union

Les types de cas de l’union sont identifiés comme l’ensemble de types de paramètres de ces constructeurs. Les types de cas de l’union sont identifiés comme l’ensemble des types de paramètres de ces méthodes d’usine.

En même temps :

Méthode TryGetValue pour chaque type de cas. La méthode retourne bool et prend un seul paramètre out d’un type qui correspond au type de cas donné de la façon suivante :

  • Si le type de cas est un type valeur Nullable, le type du paramètre doit être convertible en identité-convertible en type sous-jacent
  • Dans le cas contraire, le type doit être convertible en identité-convertible en type de casse.

Existe-t-il un avantage pour avoir un type valeur nullable parmi les types de cas, en particulier qu’un modèle de type ne peut pas utiliser le type valeur nullable comme type cible ? Il semble que nous puissions simplement dire que, si le type de paramètre/fabrique du constructeur est un type valeur nullable, le type de cas correspondant est le type sous-jacent. Ensuite, nous n’aurions pas besoin de cette clause supplémentaire pour la TryGetValue méthode, tous les paramètres out sont des types de cas.

Résolution: La suggestion est approuvée

[Résolu] État nullable par défaut de Value la propriété

Pour les types union où aucun des types de cas n’est nullable, l’état par défaut pour Value lequel « n’est pas null » plutôt que « peut-être null ».

Avec la nouvelle conception, où Value la propriété n’est pas définie dans une interface générale, mais qui est une API qui appartient spécifiquement au type déclaré, la règle indiquée ci-dessus se sent comme une sur-ingénierie. De plus, la règle forcera probablement les consommateurs à utiliser des types nullables dans des situations où les types nullables ne seraient pas utilisés.

Par exemple, considérez la déclaration d’union suivante :

union U1(int, bool, DateTime);

Selon la règle entre guillemets, l’état par défaut pour Value lequel « n’est pas null ». Mais cela ne correspond pas au comportement du type, default(U1).Value c’est null. Pour réaligner le comportement, le consommateur est forcé d’effectuer au moins un type de cas Nullable. Quelque chose comme :

union U1(int?, bool, DateTime);

Mais cela est probablement indésirable, le consommateur peut ne pas vouloir autoriser la création explicite avec int? valeur.

Proposition : Supprimez la règle entre guillemets, l’analyse nullable doit utiliser des annotations de la Value propriété pour déduire sa valeur nullabilité par défaut.

Résolution: La proposition est approuvée

[Résolu] Correspondance d’union pour Nullable d’un type de valeur union

Lorsque la valeur entrante d’un modèle est d’un type union, le contenu de la valeur union peut être « décompressé », selon le modèle.

Devons-nous étendre cette règle aux scénarios lorsque la valeur entrante d’un modèle est d’un Nullable<union type>?

Considérez le scénario suivant :

    static bool Test1(StructUnion? u)
    {
        return u is 1;
    }   

    static bool Test2(ClassUnion? u)
    {
        return u is 1;
    }   

La signification de u is 1 Test1 et Test2 est très différente. Dans Test1, il ne s’agit pas d’une correspondance union, dans Test2. Peut-être que la « correspondance union » doit « creuser » comme Nullable<T> le fait généralement la correspondance de modèle dans d’autres situations.

Si nous allons avec cela, le modèle de correspondance null de l’union par rapport Nullable<union type> à doit fonctionner comme contre les classes. C’est-à-dire que le modèle est vrai quand (!nullableValue.HasValue || nullableValue.Value.Value is null).

Résolution: La proposition est approuvée.

Que faire des API « mauvaises » ?

Que doit faire le compilateur concernant les API de correspondance union qui ressemblent à une correspondance, mais sinon « mauvais » ? Par exemple, le compilateur recherche TryGetValue/HasValue avec la signature correspondante, mais il est « incorrect », car un modificateur personnalisé requis ou nécessite une fonctionnalité inconnue, etc. Le compilateur doit-il ignorer silencieusement l’API ou signaler une erreur ? De même, l’API peut être marquée comme obsolète/expérimentale. Le compilateur doit-il signaler des diagnostics, utiliser silencieusement l’API ou ne pas utiliser l’API en mode silencieux ?

Que se passe-t-il si des types pour la déclaration union sont manquants

Que se passe-t-il si UnionAttributeou IUnionIUnion<TUnion> sont manquants ? Erreur? Synthétiser? Chose?

[Résolu] Conception de l’interface IUnion générique

Des arguments ont été faits qui IUnion<TUnion> ne doivent pas hériter ou IUnion limiter son paramètre de type à IUnion<TUnion>. Nous devrions revoir.

Résolution: L’interface IUnion<TUnion> est supprimée pour l’instant.

[Résolu] Types valeur nullables en tant que types de cas et leur interaction avec TryGetValue

Les règles ci-dessus indiquent que si un type de cas est un type valeur Nullable, le type de paramètre utilisé dans une méthode correspondante TryGetValue doit être le type sous-jacent . Cela est motivé par le fait qu’une null valeur ne serait jamais générée par cette méthode. Côté consommation, un type valeur nullable n’est pas autorisé comme modèle de type, tandis qu’une correspondance avec le type sous-jacent doit être en mesure de mapper à un appel de cette méthode.

Nous devrions confirmer que nous sommes d’accord avec ce déballage.

Résolution: Accepté/confirmé

Modèle d’accès non boxing union

Vous devez spécifier des règles précises pour trouver les API appropriéesHasValue.TryGetValue L’héritage est-il impliqué ? La lecture/écriture est-elle HasValue une correspondance acceptable ? Etc.

[Résolu] TryGetValue conversions correspondantes

La section Union Matching indique :

Pour un modèle qui implique la vérification d’un type Tspécifique, si une TryGetValue(S value) méthode est disponible et qu’il existe une conversion implicite à Spartir de T , cette méthode est utilisée pour obtenir la valeur.

L’ensemble de conversions implicites est-il limité de quelque manière que ce soit ? Par exemple, les conversions définies par l’utilisateur sont-elles autorisées ? Qu’en est-il des conversions tuples et d’autres conversions non si triviales ? Certaines d’entre elles sont même des conversions standard.

L’ensemble de TryGetValue méthodes est-il restreint d’une autre façon ? Par exemple, la section Modèles Union implique que seules les méthodes avec un type de paramètre correspondant à un type de cas sont prises en compte :

une public bool TryGetValue(out T value) méthode pour chaque type de Tcas .

Il serait bon d’avoir une réponse explicite.

Résolution: Seules les conversions d’identité implicite, de référence ou de boxe sont prises en compte

TryGetValue et l’analyse nullable

Lorsque le modèle d’accès non boxing ou HasValueTryGetValue(...) est utilisé pour interroger le contenu d’un type union (explicitement ou via une correspondance de modèle), il affecte Valuel’état de nullabilité de la même façon que s’il Value avait été vérifié directement : l’état null de Value devient « non null » sur la true branche.

L’ensemble de TryGetValue méthodes est-il limité de quelque manière que ce soit ? Par exemple, la section Modèles Union implique que seules les méthodes avec un type de paramètre correspondant à un type de cas sont prises en compte :

une public bool TryGetValue(out T value) méthode pour chaque type de Tcas .

Il serait bon d’avoir une réponse explicite.

Clarifier les règles relatives aux default valeurs des types d’union de struct

Remarque : La règle de nullabilité par défaut mentionnée ci-dessous a été supprimée.

Remarque : Les règles d’intégrité « par défaut » mentionnées ci-dessous ont été supprimées. Nous devrions confirmer que c’est ce que nous voulons.

La section Nullability indique :

Pour les types union où aucun des types de cas n’est nullable, l’état par défaut pour Value lequel « n’est pas null » plutôt que « peut-être null ».

Étant donné que, pour l’exemple ci-dessous, l’implémentation actuelle considère Values2 comme « non null » :

S2 s2 = default;

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => throw null!;
    public S2(bool x) => throw null!;
    object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}

En même temps, la section Well-formness indique :

  • Valeur par défaut : si un type union est un type valeur, il s’agit de la valeur par défaut comme null son Value.
  • Constructeur par défaut : si un type union a un constructeur nullaire (sans argument), l’union résultante a null comme son Value.

Une implémentation comme celle-ci est en contradiction avec le comportement d’analyse nullable pour l’exemple ci-dessus.

Les règles bien formées doivent-elles être ajustées ou l’état de Valuedefault « peut-être null » ? Si ce dernier est le cas, l’initialisation S2 s2 = default; doit-elle générer un avertissement de nullabilité ?

Vérifiez qu’un paramètre de type n’est jamais un type union, même s’il est contraint à un seul.

class C1 : System.Runtime.CompilerServices.IUnion
{
    private readonly object _value;
    public C1(int x) { _value = x; }
    public C1(string x) { _value = x; }
    object System.Runtime.CompilerServices.IUnion.Value => _value;
}

class Program
{
    static bool Test1<T>(T u) where T : C1
    {
        return u is int; // Not a union matching
    }   

    static bool Test2<T>(T u) where T : C1
    {
        return u is string; // Not a union matching
    }   
}

Les attributs post-condition doivent-ils affecter la nullabilité par défaut d’une instance Union ?

Remarque : La règle de nullabilité par défaut mentionnée ci-dessous a été supprimée. Et nous n’inférerons plus la nullabilité par défaut de la propriété à partir des méthodes de Value création d’union. Par conséquent, la question est obsolète/ne s’applique plus à la conception actuelle.

Pour les types union où aucun des types de cas n’est nullable, l’état par défaut pour Value lequel « n’est pas null » plutôt que « peut-être null ».

Avertissement attendu dans le scénario suivant

#nullable enable

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null!;
    public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!;
    object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}
class Program
{
    static void Test2(S1 s)
    {
       // warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
       //                 For example, the pattern 'null' is not covered.
        _ = s switch { int => 1, bool => 3 }; // 
    } 
}

Conversions d’union

[Résolu] Où appartiennent-ils parmi d’autres conversions prioritaires ?

Les conversions d’union se sentent comme une autre forme de conversion définie par l’utilisateur. Par conséquent, l’implémentation actuelle les classifie juste après une tentative d’échec de classification d’une conversion implicite définie par l’utilisateur, et, en cas d’existence, est traitée comme une autre forme de conversion définie par l’utilisateur. Cela a les conséquences suivantes :

  • Une conversion implicite définie par l’utilisateur prend la priorité sur une conversion d’union
  • Lorsque le cast explicite est utilisé dans le code, une conversion explicite définie par l’utilisateur prend la priorité sur une conversion d’union
  • Lorsqu’il n’y a pas de cast explicite dans le code, une conversion union prend la priorité sur une conversion explicite définie par l’utilisateur
struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => ...
    public S1(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static implicit operator S1(int x) => ...
}

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => ...
    public S2(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static explicit operator S2(int x) => ...
}

class Program
{
    static S1 Test1() => 10; // implicit operator S1(int x) is used
    static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
    static S2 Test3() => 10; // Union conversion S2.S2(int) is used
    static S2 Test4() => (S2)20; // explicit operator S2(int x)
}

Vous devez confirmer qu’il s’agit du comportement que nous aimons. Sinon, les règles de conversion doivent être claires.

Résolution :

Approuvé par le groupe de travail.

[Résolu] Réf-ness du paramètre du constructeur

Actuellement, la langue autorise uniquement les paramètres et in les valeurs pour les opérateurs de conversion définis par l’utilisateur. Il semble que les raisons de cette restriction s’appliquent également aux constructeurs appropriés pour les conversions d’union.

Proposition:

Ajustez la définition d’une case type constructor section ci-dessus Union types :

-For each public constructor with exactly one parameter, the type of that parameter is considered a *case type* of the union type.
+For each public constructor with exactly one **by-value or `in`** parameter, the type of that parameter is considered a *case type* of the union type.

Résolution :

Approuvé par le groupe de travail pour l’instant. Toutefois, nous pouvons envisager de « fractionner » l’ensemble de constructeurs de type cas et l’ensemble de constructeurs appropriés pour les conversions de type union.

[Résolu] Conversions nullables

La section Conversions nullables répertorie explicitement les conversions qui peuvent être utilisées comme sous-jacents. La spécification actuelle ne propose aucun ajustement de cette liste. Cela entraîne une erreur pour le scénario suivant :

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1? Test1(int x)
    {
        return x; // error CS0029: Cannot implicitly convert type 'int' to 'S1?'
    }   
}

Proposition:

Ajustez la spécification pour prendre en charge une conversion nullable implicite à partir d’une ST? conversion union. En particulier, en supposant qu’il T s’agit d’un type union, il existe une conversion implicite vers un type à partir d’un type T? ou d’une expression E s’il existe une conversion d’union d’un E type C et C est un type de cas de T. Notez qu’il n’est pas nécessaire que le type soit E un type valeur non nullable. La conversion est évaluée en tant que conversion d’union sous-jacente de à l’autre ST , suivie d’un retour à la T ligne T?

Résolution :

Approuvé.

[Résolu] Conversions levées

Voulez-nous ajuster la section conversions lifted pour prendre en charge les conversions d’union levées ? Actuellement, ils ne sont pas autorisés :

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(int? x)
    {
        return x; // error CS0029: Cannot implicitly convert type 'int?' to 'S1'
    }   

    static S1? Test2(int? y)
    {
        return y; // error CS0029: Cannot implicitly convert type 'int?' to 'S1?'
    }   
}

Résolution :

Aucune conversion d’union levée pour l’instant. Quelques notes de la discussion :

L’analogie avec les conversions définies par l’utilisateur se décompose un peu ici. En général, les unions peuvent contenir une valeur Null entrante. Il n’est pas clair si le lift doit créer une instance d’un type union avec null une valeur stockée dans celui-ci, ou s’il doit créer une null valeur de Nullable<Union>.

[Résolu] Bloquer la conversion d’union à partir d’une instance d’un type de base ?

On peut trouver le comportement actuel déroutant :

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(System.ValueType x)
    {
    }
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(System.ValueType x)
    {
        return x; // Union conversion
    }   

    static S1 Test2(System.ValueType y)
    {
        return (S1)y; // Unboxing conversion
    }   
}

Notez que le langage interdit explicitement la déclaration de conversions définies par l’utilisateur à partir d’un type de base. Par conséquent, il peut rendre la confiance pour ne pas autoriser les conversions d’union comme cela.

Résolution :

Ne faites rien de spécial pour l’instant. Les scénarios génériques ne peuvent pas être entièrement protégés de toute façon.

[Résolu] Bloquer la conversion d’union à partir d’une instance d’un type d’interface ?

On peut trouver le comportement actuel déroutant :

struct S1 : I1, System.Runtime.CompilerServices.IUnion
{
    public S1(I1 x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

interface I1 { }

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(I1 x) => throw null;
    public S2(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class C3 : System.Runtime.CompilerServices.IUnion
{
    public C3(I1 x) => throw null;
    public C3(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(I1 x)
    {
        return x; // Union conversion
    }   

    static S1 Test2(I1 x)
    {
        return (S1)x; // Unboxing
    }   

    static S2 Test3(I1 x)
    {
        return x; // Union conversion
    }   

    static S2 Test4(I1 x)
    {
        return (S2)x; // Union conversion
    }   

    static C3 Test3(I1 x)
    {
        return x; // Union conversion
    }   

    static C3 Test4(I1 x)
    {
        return (C3)x; // Reference conversion
    }   
}

Notez que le langage interdit explicitement la déclaration de conversions définies par l’utilisateur à partir d’un type de base. Par conséquent, il peut rendre la confiance pour ne pas autoriser les conversions d’union comme cela.

Résolution :

Ne faites rien de spécial pour l’instant. Les scénarios génériques ne peuvent pas être entièrement protégés de toute façon.

Espace de noms de l’interface IUnion

Le conteneur de l’espace de noms pour IUnion l’interface reste non spécifié. Si l’intention est de la conserver dans un global espace de noms, nous allons l’indiquer explicitement.

Proposition : S’il s’agit simplement d’un élément négligé, nous pourrions utiliser l’espace System.Runtime.CompilerServices de noms.

Classes en tant que Union types

[Résolu] Vérification de l’instance elle-même null

Si un type union est un type de classe, sa valeur peut être null. Qu’en est-il des vérifications null ensuite ? Le null modèle a été co-choisi pour vérifier la Value propriété, alors comment vérifier que l’union elle-même n’est pas null ?

Par exemple:

  • Lorsqu’il s’agit d’un Union struct, s is null pour une valeur de n’est trueS?que lorsque s lui-même est null.S Quand C est une Union classe, c is null pour une valeur de C?est falsequand c elle-même est null, mais c’est true quand c elle-même n’est pas nullet c.Value est null.

Voici un autre exemple :

class C1 : IUnion
{
    private readonly object? _value;

    public C1(){}
    public C1(int x) { _value = x; }
    public C1(string x) { _value = x; }
    object? IUnion.Value => _value;
}

class Program
{
    static int Test1(C1? u)
    {
        // warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
        //                 For example, the pattern 'null' is not covered.
        // This is very confusing, the switch expression is indeed not exhaustive (u itself is not
        // checked for null), but there is a case 'null => 3' in the switch expression. 
        // It looks like the only way to shut off the warning is to use 'case _'. Adding it removes
        // all benefits of exhaustiveness checking, any union case could be missing and there would
        // be no diagnostic about that.  
        return u switch { int => 1, string => 2, null => 3 };
    }
}

Cette partie de la conception est clairement optimisée autour de l’attente qu’un type union est un struct. Certaines options :

  • Tant pis. Utilisez == pour votre vérification null au lieu d’une correspondance de modèle.
  • Laissez le null modèle (et la vérification null implicite dans d’autres modèles) s’appliquer à la fois à la valeur union et à sa Value propriété : u is null ==> u == null || u.Value == null.
  • Interdire les classes d’être des types union !

[Résolu] Dérivation d’une Union classe

Lorsqu’une classe utilise une Unionclasse comme classe de base, selon la spécification actuelle, elle devient une Unionclasse elle-même. Cela se produit parce qu’il « hérite » automatiquement de l’implémentation de l’interface IUnion , il n’est pas nécessaire de le réinscrire. En même temps, les constructeurs du type dérivé définissent l’ensemble de types dans ce nouveau Union. Il est très facile d’accéder à un comportement de langage très étrange autour des deux classes :

class C1 : IUnion
{
    private readonly object _value;
    public C1(long x) { _value = x; }
    public C1(string x) { _value = x; }
    object IUnion.Value => _value;
}

class C2(int x) : C1(x);

class Program
{
    static int Test1(C1 u)
    {
        // Good
        return u switch { long => 1, string => 2, null => 3 };
    } 

    static int Test2(C2 u)
    {
        // error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'long'.
        // error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'string'.
        return u switch { long => 1, string => 2, null => 3 };
    } 
}

Certaines options :

  • Changez lorsqu’un type de classe est un Union type. Par exemple, une classe est un Union type quand toutes les valeurs true :

    • Cela est sealed dû au fait que les types dérivés ne seront pas considérés comme Uniondes types, ce qui est déroutant.
    • Aucune de ses bases n’est implémentée IUnion

    Ce n’est toujours pas parfait. Les règles sont trop subtiles. Il est facile de faire une erreur. Il n’existe aucun diagnostic sur la déclaration, mais Union la correspondance ne fonctionne pas.

  • Interdire les classes d’être des types union.

[Résolu] Opérateur is-type

L’opérateur is-type est spécifié en tant que vérification de type runtime. Syntactiquement, il ressemble beaucoup à un modèle de type, mais ce n’est pas le cas. Par conséquent, la correspondance spéciale Unionne sera pas utilisée, ce qui peut entraîner une confusion de l’utilisateur.

struct S1 : IUnion
{
    private readonly object _value;
    public S1(int x) { _value = x; }
    public S1(string x) { _value = x; }
    object IUnion.Value => _value;
}

class Program
{
    static bool Test1(S1 u)
    {
        return u is int; // warning CS0184: The given expression is never of the provided ('int') type
    }   

    static bool Test2(S1 u)
    {
        return u is string and ['1', .., '2']; // Good
    }   
}

En cas d’union récursive, le modèle de type peut ne pas donner d’avertissement, mais il ne fera toujours pas ce que l’utilisateur pourrait penser qu’il ferait.

Résolution: Doit fonctionner comme modèle de type.

Modèle de liste

Le modèle de liste échoue toujours avec Union la correspondance :

struct S1 : IUnion
{
    private readonly object _value;
    public S1(int[] x) { _value = x; }
    public S1(string[] x) { _value = x; }
    object IUnion.Value => _value;
}

class Program
{
    static bool Test1(S1 u)
    {
        // error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found.
        // error CS0021: Cannot apply indexing with [] to an expression of type 'object'
        return u is [10];
    }   
}

static class Extensions
{
    extension(object o)
    {
        public int Length => 0;
    }
}

Autres questions

  • L’utilisation des constructeurs dans les conversions d’union et l’utilisation de TryGetValue(...) la correspondance des modèles d’union sont spécifiées pour être lent lorsque plusieurs s’appliquent : ils ne sélectionnent qu’un. Cela ne devrait pas être important en fonction des règles bien formées, mais sommes-nous à l’aise avec elle ?
  • La spécification dépend subtilement de l’implémentation de la IUnion.Value propriété plutôt que d’une Value propriété trouvée sur le type union lui-même. Cela est destiné à offrir une plus grande flexibilité aux types existants (qui peuvent avoir leur propre Value propriété pour d’autres utilisations) pour implémenter le modèle. Mais il est maladroit et incohérent avec la façon dont d’autres membres sont trouvés et utilisés directement sur le type d’union. Devrions-nous apporter une modification ? Voici d’autres options :
    • Exiger des types d’union pour exposer une propriété publique Value .
    • Préférez une propriété publique Value s’il existe, mais revenez à l’implémentation IUnion.Value si ce n’est pas (similaire aux GetEnumerator règles).
  • La syntaxe de déclaration d’union proposée n’est pas universellement aimée, en particulier lorsqu’il s’agit d’exprimer les types de cas. Les alternatives jusqu’à présent rencontrent également des critiques, mais il est possible que nous finissent par apporter un changement. Certaines préoccupations principales ont été exprimées au sujet de l’actuel :
    • Les virgules comme séparateurs entre les types de cas peuvent sembler impliquer que l’ordre importe.
    • Les listes entre parenthèses ressemblent trop aux constructeurs principaux (malgré le fait qu’elles n’ont pas de noms de paramètres).
    • Trop différent des énumérations, qui ont leurs « cas » dans les accolades.
  • Bien que les déclarations d’union génèrent des structs avec un seul champ de référence, ils sont toujours quelque peu susceptibles d’être utilisés dans un contexte simultané. Par exemple, si un membre de fonction défini par l’utilisateur fait plusieurs références this plusieurs fois, la variable contenante peut avoir été réaffectée dans son ensemble par un autre thread entre les deux accès. Le compilateur peut générer du code à copier this dans un local si nécessaire. Faut-il le faire ? En général, quel degré de résilience concurrentiel est souhaitable et raisonnablement réalisable ?