2010-12-09 94 views
11

J'ai joué avec des structures comme mécanisme pour valider implicitement des objets de valeur complexe, ainsi que des structures génériques autour de classes plus complexes pour garantir des valeurs valides. Je suis un peu ignorant des conséquences sur la performance, alors j'espère que vous pourrez tous m'aider. Par exemple, si je devais faire quelque chose comme l'injection d'un objet de domaine dans un wrapper de type valeur, cela causerait-il des problèmes? Pourquoi? Je comprends la différence entre les types de valeur et les types de référence, et mon but ici est de tirer parti du comportement différent des types de valeur. De quoi ai-je besoin pour faire cela de façon responsable?Quand est-ce que l'utilisation de structures C# (types de valeurs) sacrifie les performances?

Voici une idée extrêmement élémentaire de quelque chose que je pensais.

public struct NeverNull<T> 
    where T: class, new() 
{ 

    private NeverNull(T reference) 
    { 
     _reference = reference; 
    } 

    private T _reference; 

    public T Reference 
    { 
     get 
     { 
      if(_reference == null) 
      { 
       _reference = new T(); 
      } 
      return _reference; 
     } 
     set 
     { 
      _reference = value; 
     } 
    } 

    public static implicit operator NeverNull<T>(T reference) 
    { 
     return new NeverNull<T>(reference); 
    } 

    public static implicit operator T(NeverNull<T> value) 
    { 
     return value.Reference; 
    } 
} 
+0

c'est la 1ère fois, je vois ce type de question – TalentTuner

Répondre

10

Eh bien, une chose est méchant que cela ne se comporte pas comme on pourrait s'y attendre naïvement:

NeverNull<Foo> wrapper1 = new NeverNull<Foo>(); 
NeverNull<Foo> wrapper2 = wrapper1; 

Foo foo1 = wrapper1; 
Foo foo2 = wrapper2; 

Cela va créer deux cas de Foo parce que la version originale a été copiée, avant wrapper1 a créé une instance.

Fondamentalement, vous avez affaire à une structure mutable - qui est presque jamais une bonne chose à avoir. De plus, je ne suis généralement pas intéressé par les conversions implicites. Il semble que vous essayez d'obtenir un code d'apparence magique ici ... et je suis généralement contre ce genre de chose. Peut-être il est logique pour votre cas d'utilisation particulier, mais je ne peux pas penser à l'endroit où je voudrais personnellement l'utiliser.

+0

J'étais conscient du comportement que vous avez décrit. Cependant, si c'était indésirable, je vois une méthode pour contourner le «problème de la copie». Au lieu de dans le cas de NeverNull, les références pourraient être stockées dans un dictionnaire statique. Ensuite, le NeverNull stockerait le hashcode dans le dictionnaire. Il me semble que cela aboutirait au même type de fonctionnalité sans copier les références. Le rôle de la distribution implicite est de faciliter le travail avec le type de référence tout en préservant une contrainte non nullable. – smartcaveman

+0

Cela soulève une autre question. Les membres statiques des structures se comportent-ils différemment des membres statiques des classes? J'apprécie ton aide. – smartcaveman

+1

@smartcaveman: On dirait que vous ajoutez des couches de magie les unes sur les autres ... Je trouve rarement que je suis dans une situation où je serais heureux avec une nouvelle instance "vide" en cours de construction si elle n'était pas t autrement présent. En ce qui concerne les membres statiques - non, ils sont fondamentalement les mêmes entre les structures et les classes. –

2

La peine principale est avec boxe pour struct. En outre, ils sont passés par valeur si une grande struct lorsqu'il est passé à une méthode devra être copié:

MyStruct st; 
foo.Bar(st); // st is copied 
+0

+1 pour faire face un problème de performance, ce qui est ce que cette question se pose au sujet. Cet article MSDN (http://msdn.microsoft.com/en-us/library/ah19swz4%28VS.71%29.aspx) décrit brièvement les situations où il peut être plus efficace d'utiliser un type de valeur, bien qu'il ne soit pas adressez-vous directement à l'inverse: quand n'est-il pas efficace d'utiliser un type de valeur. –

2

Ok, juste une note sur ce qui précède.

MyStruct st; foo.Bar (st); // st est copié

Ceci n'est pas une boîte, sauf si le paramètre de Bar est objet par exemple.

void Bar(MyStruct parameter){} 

n'inclurait pas le type de valeur.

Les paramètres sont passés par valeur dans C# par défaut, sauf si vous utilisez le mot-clé ref ou out. Les paramètres passés par valeur sont copiés. La différence entre passer la structure et l'objet est ce qui est passé. Avec un type de valeur, la valeur réelle est copiée, ce qui signifie qu'un nouveau type de valeur est créé, de sorte que vous obtenez une copie. Avec un type de référence, la référence au type de référence est transmise. Indices dans le nom je suppose :)

Donc, il y a un hit de performance pour les structures car toute la structure est copiée sauf si vous utilisez le mot clé ref/out, et Si vous faites cela de manière intensive, je pense que votre code doit être examiné.

La boxe est le processus d'affectation d'un type de valeur à une variable de type référence. Un nouveau type de référence (objet) est créé et une copie du type de valeur affecté à celui-ci.J'ai en quelque sorte eu ce que vous faisiez dans le code original, mais il semble résoudre un problème simple avec un problème qui a beaucoup de complexités implicites plutôt qu'explicites.

+0

Je ne l'ai pas dit, j'ai mentionné 2 numéros, un boxing, une copie, il est dit clairement dans le commentaire – Aliostad

1

Un autre problème de performances survient lorsque vous placez des structures dans des collections. Par exemple, imaginez que vous avez un List<SomeStruct> et que vous souhaitez modifier la propriété Prop1 du premier élément de la liste. L'inclinaison initiale est d'écrire ceci:

List<SomeStruct> MyList = CreateList(); 
MyList[0].Prop1 = 42; 

Cela ne va pas compiler. Afin de faire ce travail, vous devez écrire:

SomeStruct myThing = MyList[0]; 
myThing.Prop1 = 42; 
MyList[0] = myThing.Prop1; 

Cela provoque deux problèmes (principalement). Tout d'abord, vous finissez par copier l'ensemble de la structure deux fois: une fois dans votre instance de travail myThing, puis revenez à la liste. Le deuxième problème est que vous ne pouvez pas le faire dans un foreach parce qu'il modifie la collection et entraînera l'énumérateur à lever une exception. À propos, votre chose NeverNull a un comportement plutôt étrange. Il est possible de définir la propriété Reference sur null. Il me semble très très étrange que cette déclaration:

var Contradiction = new NeverNull<object>(null); 

Est valide.

Je serais intéressé de connaître les raisons que vous avez pour essayer de créer ce type de struct.

+0

La référence La propriété peut être définie sur null, mais la valeur null ne peut jamais être accédée.La définition de Reference = null serait en fait la même chose que la définition de la propriété de référence à sa valeur par défaut, un nouveau T() – smartcaveman

+0

Ma solution serait de créer une méthode d'extension, à List où T: struct, avec une signature void ChangeValue (int index, Action changeAction) – smartcaveman

+0

@smartcaveman: Vous auriez besoin d'un délégué qui a pris un 'réf 'paramètre de typ e 'T'. L'idée d'une méthode 'AccessValueAt' qui passerait un élément de liste comme paramètre' ref' est une bonne idée, mais la seule façon de parvenir à une telle chose est que la méthode en question transmette l'élément approprié du tableau de sauvegarde un paramètre 'ref', et il n'y a aucun moyen que les classes dérivées de' List' puissent avoir accès au tableau nécessaire. – supercat

7

Comme Jon souligne à juste titre, le problème ici est que le comportement du type est inattendu, pas qu'il est lent. Du point de vue des performances, la surcharge de l'encapsuleur struct autour de la référence doit être très faible. Si ce que vous voulez faire est de représenter un type de référence non nullable alors une struct est un moyen raisonnable de le faire; cependant, je serais enclin à faire la struct immuable en perdant la fonction « création automatique »:

public struct NeverNull<T> where T: class 
{ 
    private NeverNull(T reference) : this() 
    { 
     if (reference == null) throw new Exception(); // Choose the right exception 
     this.Reference = reference; 
    } 

    public T Reference { get; private set; } 

    public static implicit operator NeverNull<T>(T reference) 
    { 
     return new NeverNull<T>(reference); 
    } 

    public static implicit operator T(NeverNull<T> value) 
    { 
     return value.Reference; 
    } 
} 

Faire l'appelant responsable de fournir une référence valable; S'ils veulent en "faire" un, laissez-les.

Notez également que les opérateurs de conversion génériques peuvent vous donner des résultats inattendus. Vous devriez lire la spécification sur les opérateurs de conversion et le comprendre à fond. Par exemple, vous ne pouvez pas créer un wrapper non nul autour de "object" et ensuite convertir implicitement cette chose en conversion de déballage; toute conversion implicite en objet sera une conversion de boxe sur la structure. Vous ne pouvez pas "remplacer" une conversion intégrée du langage C#.

+0

Savez-vous où trouver la documentation officielle sur les opérateurs de conversion génériques? Je suis familier avec tout ici http://msdn.microsoft.com/en-us/library/09479473(v=VS.80).aspx, mais je n'ai rien vu avec un accent particulier sur les génériques. – smartcaveman

+1

@smartcaveman: Lire les sections 6.4 et 10.10.3 de la spécification. –

+0

Quel est le point de cette structure? Si l'on dit 'NeverNull

SupposedlyNonNullForm = par défaut (NeverNull )', alors 'SupposedlyNonNullForm.Reference 'sera' null'. En outre, cette variable 'NeverNull ' peut être copiée très bien à tout autre 'NeverNull ' sans plainte. La seule façon de voir un 'NeverNull ' comme utile serait si 'T' était un type de classe immuable, il y avait une propriété' DefaultReference' statique mutable, et la propriété 'Reference' renvoyait' DefaultRefernece' quand le champ de sauvegarde était nul. – supercat

2

Les réponses à cette question semblent s'être éloignées de la discussion sur les performances et abordent plutôt les dangers des types de valeurs mutables. Juste au cas où vous trouveriez ceci utile, voici une implémentation que j'ai lancée ensemble qui fait quelque chose de similaire à votre exemple original en utilisant un wrapper de type valeur immuable. La différence est que mon type de valeur ne fait pas directement référence à l'objet auquel il se rapporte; à la place, il contient une clé et des références aux délégués qui effectuent une recherche en utilisant la clé (TryGetValueFunc) ou un create en utilisant la clé.(Remarque: mon encapsuleur contenait une référence à un objet IDictionary dans mon implémentation d'origine, mais je l'ai remplacé par un délégué TryGetValueFunc juste pour le rendre un peu plus flexible, même si cela peut être plus confus et je ne suis pas sûr à 100% cela n'a pas ouvert une sorte de défaut). Notez toutefois que cela peut entraîner un comportement inattendu (en fonction de ce que vous attendez) si vous manipulez les structures de données sous-jacentes auxquelles accède l'encapsuleur.

Voici une exemple de travail complet, ainsi que d'un exemple d'utilisation d'un programme de console:

public delegate bool TryGetValueFunc<TKey, TValue>(TKey key, out TValue value); 

public struct KeyedValueWrapper<TKey, TValue> 
{ 
    private bool _KeyHasBeenSet; 
    private TKey _Key; 
    private TryGetValueFunc<TKey, TValue> _TryGetValue; 
    private Func<TKey, TValue> _CreateValue; 

    #region Constructors 

    public KeyedValueWrapper(TKey key) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _TryGetValue = null; 
     _CreateValue = null; 
    } 

    public KeyedValueWrapper(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _TryGetValue = tryGetValue; 
     _CreateValue = null; 
    } 

    public KeyedValueWrapper(TKey key, Func<TKey, TValue> createValue) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _TryGetValue = null; 
     _CreateValue = createValue; 
    } 

    public KeyedValueWrapper(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _TryGetValue = tryGetValue; 
     _CreateValue = createValue; 
    } 

    public KeyedValueWrapper(TryGetValueFunc<TKey, TValue> tryGetValue) 
    { 
     _Key = default(TKey); 
     _KeyHasBeenSet = false; 
     _TryGetValue = tryGetValue; 
     _CreateValue = null; 
    } 

    public KeyedValueWrapper(TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue) 
    { 
     _Key = default(TKey); 
     _KeyHasBeenSet = false; 
     _TryGetValue = tryGetValue; 
     _CreateValue = createValue; 
    } 

    public KeyedValueWrapper(Func<TKey, TValue> createValue) 
    { 
     _Key = default(TKey); 
     _KeyHasBeenSet = false; 
     _TryGetValue = null; 
     _CreateValue = createValue; 
    } 

    #endregion 

    #region "Change" methods 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, _TryGetValue, _CreateValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, tryGetValue, _CreateValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, _TryGetValue, createValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, tryGetValue, createValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TryGetValueFunc<TKey, TValue> tryGetValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(_Key, tryGetValue, _CreateValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(_Key, tryGetValue, createValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(_Key, _TryGetValue, createValue); 
    } 

    #endregion 

    public TValue Value 
    { 
     get 
     { 
      if (!_KeyHasBeenSet) 
       throw new InvalidOperationException("A key must be specified."); 

      if (_TryGetValue == null) 
       throw new InvalidOperationException("A \"try get value\" delegate must be specified."); 

      // try to find a value in the given dictionary using the given key 
      TValue value; 
      if (!_TryGetValue(_Key, out value)) 
      { 
       if (_CreateValue == null) 
        throw new InvalidOperationException("A \"create value\" delegate must be specified."); 

       // if not found, create a value 
       value = _CreateValue(_Key); 
      } 
      // then return that value 
      return value; 
     } 
    } 
} 

class Foo 
{ 
    public string ID { get; set; } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     var dictionary = new Dictionary<string, Foo>(); 

     Func<string, Foo> createValue = (key) => 
     { 
      var foo = new Foo { ID = key }; 
      dictionary.Add(key, foo); 
      return foo; 
     }; 

     // this wrapper object is not useable, since no key has been specified for it yet 
     var wrapper = new KeyedValueWrapper<string, Foo>(dictionary.TryGetValue, createValue); 

     // create wrapper1 based on the wrapper object but changing the key to "ABC" 
     var wrapper1 = wrapper.Change("ABC"); 
     var wrapper2 = wrapper1; 

     Foo foo1 = wrapper1.Value; 
     Foo foo2 = wrapper2.Value; 

     Console.WriteLine("foo1 and foo2 are equal? {0}", object.ReferenceEquals(foo1, foo2)); 
     // Output: foo1 and foo2 are equal? True 

     // create wrapper1 based on the wrapper object but changing the key to "BCD" 
     var wrapper3 = wrapper.Change("BCD"); 
     var wrapper4 = wrapper3; 

     Foo foo3 = wrapper3.Value; 
     dictionary = new Dictionary<string, Foo>(); // throw a curve ball by reassigning the dictionary variable 
     Foo foo4 = wrapper4.Value; 

     Console.WriteLine("foo3 and foo4 are equal? {0}", object.ReferenceEquals(foo3, foo4)); 
     // Output: foo3 and foo4 are equal? True 

     Console.WriteLine("foo1 and foo3 are equal? {0}", object.ReferenceEquals(foo1, foo3)); 
     // Output: foo1 and foo3 are equal? False 
    } 
} 

mise en œuvre alternative à l'aide IDictionary<string, Foo> au lieu de TryGetValueFunc<string, Foo>. Notez le contre-exemple, je mets dans le code d'utilisation:

public struct KeyedValueWrapper<TKey, TValue> 
{ 
    private bool _KeyHasBeenSet; 
    private TKey _Key; 
    private IDictionary<TKey, TValue> _Dictionary; 
    private Func<TKey, TValue> _CreateValue; 

    #region Constructors 

    public KeyedValueWrapper(TKey key) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _Dictionary = null; 
     _CreateValue = null; 
    } 

    public KeyedValueWrapper(TKey key, IDictionary<TKey, TValue> dictionary) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _Dictionary = dictionary; 
     _CreateValue = null; 
    } 

    public KeyedValueWrapper(TKey key, Func<TKey, TValue> createValue) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _Dictionary = null; 
     _CreateValue = createValue; 
    } 

    public KeyedValueWrapper(TKey key, IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue) 
    { 
     _Key = key; 
     _KeyHasBeenSet = true; 
     _Dictionary = dictionary; 
     _CreateValue = createValue; 
    } 

    public KeyedValueWrapper(IDictionary<TKey, TValue> dictionary) 
    { 
     _Key = default(TKey); 
     _KeyHasBeenSet = false; 
     _Dictionary = dictionary; 
     _CreateValue = null; 
    } 

    public KeyedValueWrapper(IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue) 
    { 
     _Key = default(TKey); 
     _KeyHasBeenSet = false; 
     _Dictionary = dictionary; 
     _CreateValue = createValue; 
    } 

    public KeyedValueWrapper(Func<TKey, TValue> createValue) 
    { 
     _Key = default(TKey); 
     _KeyHasBeenSet = false; 
     _Dictionary = null; 
     _CreateValue = createValue; 
    } 

    #endregion 

    #region "Change" methods 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, _Dictionary, _CreateValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, IDictionary<TKey, TValue> dictionary) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, dictionary, _CreateValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, _Dictionary, createValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(key, dictionary, createValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(IDictionary<TKey, TValue> dictionary) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(_Key, dictionary, _CreateValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(_Key, dictionary, createValue); 
    } 

    public KeyedValueWrapper<TKey, TValue> Change(Func<TKey, TValue> createValue) 
    { 
     return new KeyedValueWrapper<TKey, TValue>(_Key, _Dictionary, createValue); 
    } 

    #endregion 

    public TValue Value 
    { 
     get 
     { 
      if (!_KeyHasBeenSet) 
       throw new InvalidOperationException("A key must be specified."); 

      if (_Dictionary == null) 
       throw new InvalidOperationException("A dictionary must be specified."); 

      // try to find a value in the given dictionary using the given key 
      TValue value; 
      if (!_Dictionary.TryGetValue(_Key, out value)) 
      { 
       if (_CreateValue == null) 
        throw new InvalidOperationException("A \"create value\" delegate must be specified."); 

       // if not found, create a value and add it to the dictionary 
       value = _CreateValue(_Key); 
       _Dictionary.Add(_Key, value); 
      } 
      // then return that value 
      return value; 
     } 
    } 
} 

class Foo 
{ 
    public string ID { get; set; } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     // this wrapper object is not useable, since no key has been specified for it yet 
     var wrapper = new KeyedValueWrapper<string, Foo>(new Dictionary<string, Foo>(), (key) => new Foo { ID = key }); 

     // create wrapper1 based on the wrapper object but changing the key to "ABC" 
     var wrapper1 = wrapper.Change("ABC"); 
     var wrapper2 = wrapper1; 

     Foo foo1 = wrapper1.Value; 
     Foo foo2 = wrapper2.Value; 

     Console.WriteLine("foo1 and foo2 are equal? {0}", object.ReferenceEquals(foo1, foo2)); 
     // Output: foo1 and foo2 are equal? True 

     // create wrapper1 based on the wrapper object but changing the key to "BCD" 
     var wrapper3 = wrapper.Change("BCD"); 
     var wrapper4 = wrapper3; 

     Foo foo3 = wrapper3.Value; 
     Foo foo4 = wrapper4.Value; 

     Console.WriteLine("foo3 and foo4 are equal? {0}", object.ReferenceEquals(foo3, foo4)); 
     // Output: foo3 and foo4 are equal? True 

     Console.WriteLine("foo1 and foo3 are equal? {0}", object.ReferenceEquals(foo1, foo3)); 
     // Output: foo1 and foo3 are equal? False 


     // Counter-example: manipulating the dictionary instance that was provided to the wrapper can disrupt expected behavior 
     var dictionary = new Dictionary<string, Foo>(); 

     var wrapper5 = wrapper.Change("CDE", dictionary); 
     var wrapper6 = wrapper5; 

     Foo foo5 = wrapper5.Value; 
     dictionary.Clear(); 
     Foo foo6 = wrapper6.Value; 

     // one might expect this to be true: 
     Console.WriteLine("foo5 and foo6 are equal? {0}", object.ReferenceEquals(foo5, foo6)); 
     // Output: foo5 and foo6 are equal? False 
    } 
} 
+0

Merci pour cela. Quelque chose de similaire a été discuté dans la section des commentaires de l'article de Jon. – smartcaveman