des paramètres de type struct ne doivent pas être attribués

9

J'ai remarqué un comportement bizarre dans mon code lors du commentaire accidentel d'une ligne dans une fonction lors de la révision du code. C'était très difficile à reproduire mais je vais illustrer un exemple similaire ici.

J'ai cette classe de test:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

il n'y a pas d'affectation à la messagerie électronique dans GetOutlaquelle cela générerait normalement une erreur:

Le paramètre de sortie 'email' doit être affecté à avant que le contrôle ne quitte la méthode actuelle

Cependant, si EmailAddress est dans une structure dans un assembly séparé, aucune erreur n'est créée et tout se compile correctement.

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

Pourquoi le compilateur n'applique-t-il pas qu'Email doit être assigné? Pourquoi ce code compile-t-il si la structure est créée dans un assembly séparé, mais il ne compile pas si la structure est définie dans l'assembly existant?

johnny 5
la source
2
Si vous utilisez une classe, vous devez «nouveau» une instance de l'objet. Ce n'est pas obligatoire pour les structures. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… (recherchez ce texte sur cette page en particulier: contrairement aux classes, les structures peuvent être instanciées sans utiliser le nouvel operato)
Dortimer
1
Dès que votre structure Dog reçoit une variable, elle ne se compile pas :)
André Sanson
Dans cet exemple, avec struct Dog{}, tout va bien.
Henk Holterman
2
@ johnny5 Ensuite, montrez l'exemple.
André Sanson
1
OK, c'est intéressant. Reproduit avec une application Core 3 Console et une bibliothèque de classe .Standard.
Henk Holterman

Réponses:

12

TLDR: Il s'agit d'un bug connu de longue date. J'ai écrit pour la première fois à ce sujet en 2010:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

Il est inoffensif et vous pouvez l'ignorer en toute sécurité, et félicitez-vous d'avoir trouvé un bug quelque peu obscur.

Pourquoi le compilateur n'applique-t- Emailil pas une attribution définitive?

Oh, c'est le cas, d'une manière. Il a juste une mauvaise idée de quelle condition implique que la variable est définitivement assignée, comme nous le verrons.

Pourquoi ce code compile-t-il si la structure est créée dans un assembly séparé, mais il ne compile pas si la structure est définie dans l'assembly existant?

C'est le noeud du bug. Le bogue est une conséquence de l'intersection de la façon dont le compilateur C # vérifie l'affectation définie sur les structures et comment le compilateur charge les métadonnées à partir des bibliothèques.

Considère ceci:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

OK, à ce stade, que savons-nous? fest un alias pour une variable de type Foo, donc le stockage a déjà été alloué et est définitivement au moins dans l'état où il est sorti de l'allocateur de stockage. S'il y avait une valeur placée dans la variable par l'appelant, cette valeur est là.

De quoi avons-nous besoin? Nous exigeons que cela fsoit définitivement attribué à tout moment où le contrôle part Mnormalement. Vous vous attendez donc à quelque chose comme:

void M(out Foo f)
{
  f = new Foo();
}

qui définit f.xet f.yà leurs valeurs par défaut. Mais qu'en est-il?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

Cela devrait aussi être bien. Mais, et voici le kicker, pourquoi devons-nous attribuer les valeurs par défaut uniquement pour les éliminer un instant plus tard? Le vérificateur d'affectation définitive de C # vérifie si chaque champ est affecté! C'est légal:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

Et pourquoi cela ne devrait-il pas être légal? C'est un type de valeur. fest une variable, et elle contient déjà une valeur de type valide Foo, nous allons donc définir les champs, et nous avons terminé, non?

Droite. Alors quel est le bug?

Le bogue que vous avez découvert est: pour réduire les coûts, le compilateur C # ne charge pas les métadonnées pour les champs privés des structures qui se trouvent dans les bibliothèques référencées . Ces métadonnées peuvent être énormes , et cela ralentirait le compilateur pour très peu de gain pour tout charger en mémoire à chaque fois.

Et maintenant, vous devriez pouvoir déduire la cause du bug que vous avez trouvé. Lorsque le compilateur vérifie si le paramètre out est définitivement attribué, il compare le nombre de champs connus au nombre de champs qui ont été définitivement initialisés et, dans votre cas, il ne connaît que les champs publics zéro car les métadonnées de champ privé n'ont pas été chargées . Le compilateur conclut "zéro champs requis, zéro champs initialisés, nous sommes bons."

Comme je l'ai dit, ce bug existe depuis plus d'une décennie et des gens comme vous le redécouvrent occasionnellement et le signalent. Il est inoffensif et il est peu probable qu'il soit réparé car sa réparation présente un avantage presque nul mais un coût de performance élevé.

Et bien sûr, le bogue ne fait pas de reproches pour les champs privés de structures qui sont en code source dans votre projet, car évidemment le compilateur a déjà des informations sur les champs privés à portée de main.

Eric Lippert
la source
@ johnny5: Vous ne devriez pas avoir d'erreurs. Voir dotnetfiddle.net/ZEKiUk . Pouvez-vous poster une simple repro?
Eric Lippert
1
Merci pour le violon parce que j'ai défini x et y comme propriétés au lieu de membres
johnny 5
1
@ johnny5: Si vous venez de définir une propriété de style C # 1.0 normale, du point de vue du vérificateur d'affectation défini, c'est une méthode, pas un champ. Si vous avez défini une propriété automatique de style C # 3.0+, le compilateur sait qu'il existe un champ privé le soutenant; les règles d'attribution définitive de cette chose ont été modifiées au fil des ans et je ne me souviens pas maintenant des règles exactes.
Eric Lippert
Si vous utilisez un à la System.TimeSpanplace, les erreurs surviennent: error CS0269: Use of unassigned out parameter 'email'et error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. Il n'y a qu'un seul champ non statique de TimeSpan, à savoir _ticks. C'est internalà son assemblage mscorlib. Cet assemblage est-il spécial? Idem avec System.DateTime, et son domaine estprivate
Jeppe Stig Nielsen
@JeppeStigNielsen: Je ne sais pas ce qui se passe avec ça! Si vous le comprenez, faites-le moi savoir.
Eric Lippert
1

Bien que cela ressemble à un bug, cela a du sens.

L''erreur manquante 'n'apparaît que lors de l'utilisation d'une bibliothèque de classes. Et une bibliothèque de classes peut avoir été écrite dans un autre langage .net, par exemple VB.Net. Le `` suivi d'affectation défini '' est une fonctionnalité de C #, pas du cadre.

Donc, dans l'ensemble, je ne pense pas que ce soit un bug, mais je ne connais pas de déclaration faisant autorité pour cela.

Henk Holterman
la source
Si vous importez un assembly en C #, même si la structure peut se trouver dans un assembly écrit dans un autre langage, le code l'utilisant est toujours en c #, donc ne devrait-il pas utiliser un suivi d'affectation défini.
johnny 5
1
Pas nécessairement. C # ne vous permettra pas d'utiliser une variable unitaire (locale) mais en même temps le framework garantit qu'elle sera mise à 0 ( default(T)). Il n'y a donc aucune violation de la sécurité de la mémoire ou quelque chose de similaire.
Henk Holterman
3
Je peux faire une telle déclaration faisant autorité. :) C'est un bug connu de longue date.
Eric Lippert
1
@EricLippert Merci, j'espérais que vous verriez cela à un moment donné
johnny 5