2008-09-23 8 views
15

C# 6 Mise à jourpièges possibles d'utilisation de cette (méthode d'extension en fonction) sténographie

En C#6 ?. is now a language feature:

// C#1-5 
propertyValue1 = myObject != null ? myObject.StringProperty : null; 

// C#6 
propertyValue1 = myObject?.StringProperty; 

La question ci-dessous applique toujours aux anciennes versions, mais si le développement d'une nouvelle application à l'aide le nouvel opérateur ?. est une bien meilleure pratique.

Original Question:

Je veux régulièrement accéder à des propriétés des objets éventuellement nuls:

string propertyValue1 = null; 
if(myObject1 != null) 
    propertyValue1 = myObject1.StringProperty; 

int propertyValue2 = 0; 
if(myObject2 != null) 
    propertyValue2 = myObject2.IntProperty; 

Et ainsi de suite ...

J'utilise si souvent que j'ai extrait pour cela.

Vous pouvez raccourcir cela dans une certaine mesure avec une ligne si:

propertyValue1 = myObject != null ? myObject.StringProperty : null; 

Cependant c'est un peu maladroit, surtout si la mise en beaucoup de propriétés ou si plus d'un niveau peut être nul, par exemple:

propertyValue1 = myObject != null ? 
    (myObject.ObjectProp != null ? myObject.ObjectProp.StringProperty) : null : null; 

Ce que je veux vraiment est la syntaxe ?? de style, qui fonctionne très bien pour les types null directement:

int? i = SomeFunctionWhichMightReturnNull(); 
propertyValue2 = i ?? 0; 

Alors je suis venu avec ce qui suit:

public static TResult IfNotNull<T, TResult>(this T input, Func<T, TResult> action, TResult valueIfNull) 
    where T : class 
{ 
    if (input != null) return action(input); 
    else return valueIfNull; 
} 

//lets us have a null default if the type is nullable 
public static TResult IfNotNull<T, TResult>(this T input, Func<T, TResult> action) 
    where T : class 
    where TResult : class 
{ return input.IfNotNull(action, null); } 

Cela nous me permet cette syntaxe:

propertyValue1 = myObject1.IfNotNull(x => x.StringProperty); 
propertyValue2 = myObject2.IfNotNull(x => x.IntProperty, 0); 

//or one with multiple levels 
propertyValue1 = myObject.IfNotNull( 
    o => o.ObjectProp.IfNotNull(p => p.StringProperty)); 

Cela simplifie ces appels, mais je ne suis pas sûr de vérifier ce type de méthode d'extension - Cela rend le code un peu plus facile à lire, mais au prix de l'extension de l'objet. Cela apparaîtrait sur tout, bien que je puisse le mettre dans un espace de noms spécifiquement référencé.

Cet exemple est plutôt simple, un peu plus complexe comparerions deux propriétés de l'objet nullables:

if((obj1 == null && obj2 == null) || 
    (obj1 != null && obj2 != null && obj1.Property == obj2.Property)) 
    ... 

//becomes 
if(obj1.NullCompare(obj2, (x,y) => x.Property == y.Property) 
    ... 

Quels sont les pièges de l'utilisation des extensions de cette façon? Les autres codeurs sont-ils susceptibles d'être confus? Est-ce juste un abus d'extensions?


Je suppose que ce que je veux vraiment ici est un compilateur/extension linguistique:

propertyValue1 = myObject != null ? myObject.StringProperty : null; 

//becomes 
propertyValue1 = myObject?StringProperty; 

Cela rendrait le cas complexe beaucoup plus facile:

propertyValue1 = myObject != null ? 
    (myObject.ObjectProp != null ? myObject.ObjectProp.StringProperty) : null 

//becomes 
propertyValue1 = myObject?ObjectProp?StringProperty; 

Cela ne fonctionne que pour la valeur types, mais vous pouvez renvoyer des équivalents NULL:

int? propertyValue2 = myObject?ObjectProp?IntProperty; 

//or 

int propertyValue3 = myObject?ObjectProp?IntProperty ?? 0; 

Répondre

16

Nous sommes venus indépendamment avec le le même nom et la même implémentation de la méthode d'extension: Null-propagating extension method. Nous ne pensons donc pas que ce soit déroutant ou abusant des méthodes de vulgarisation.

Je voudrais écrire votre "plusieurs niveaux" par exemple avec enchaînant comme suit:

propertyValue1 = myObject.IfNotNull(o => o.ObjectProp).IfNotNull(p => p.StringProperty); 

Il y a une now-closed bug on Microsoft Connect qui a proposé? "" comme un nouvel opérateur C# qui effectuerait cette propagation nulle. Mads Torgersen (de l'équipe linguistique C#) a brièvement expliqué pourquoi ils ne l'appliqueraient pas.

+0

Oui, j'ai demandé Mads directement à TechEd - fondamentalement cette fonctionnalité manque toujours la coupe, ils pourraient encore l'ajouter une future version C# – Keith

+0

@Keith Merci pour la mise à jour! (Il ne peut pas faire de mal de rappeler à l'équipe C# que les clients trouveraient cela utile.) –

+0

Ils sont maintenant sérieusement le considérer: http://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/ at-last-c-is-getting-parfois-appelé-the-safe-navigation-operator.aspx –

0

Personnellement, même après toutes vos explications, je ne me souviens pas comment diable cela fonctionne:

if(obj1.NullCompare(obj2, (x,y) => x.Property == y.Property) 

Cela pourrait être parce que je n'ai aucune expérience C#; Cependant, je pourrais lire et comprendre tout le reste dans votre code. Je préfère garder le langage de code agnostique (en particulier pour les choses triviales) afin que demain, un autre développeur pourrait le changer en une nouvelle langue sans trop d'informations sur la langue existante.

11

Si vous constatez que vous devez vérifier très souvent si une référence à un objet est null, il se peut que vous deviez utiliser le Null Object Pattern. Dans ce modèle, au lieu d'utiliser null pour gérer le cas où vous n'avez pas d'objet, vous implémentez une nouvelle classe avec la même interface mais avec des méthodes et des propriétés qui renvoient des valeurs par défaut adéquates.

1

il rend le code un peu plus facile à lire, mais au prix de l'extension de l'objet. Cela apparaîtrait sur tout,

Notez que vous n'êtes pas réellement étendre quoi que ce soit (sauf théoriquement).

propertyValue2 = myObject2.IfNotNull(x => x.IntProperty, 0); 

généreront un code IL exactement comme s'il était écrit:

ExtentionClass::IfNotNull(myObject2, x => x.IntProperty, 0); 

Il n'y a pas de « frais généraux » ajouté aux objets à l'appui.

5

Comment est

propertyValue1 = myObject.IfNotNull(o => o.ObjectProp.IfNotNull(p => p.StringProperty)); 

plus facile à lire et à écrire que

if(myObject != null && myObject.ObjectProp != null) 
    propertyValue1 = myObject.ObjectProp.StringProperty; 

Jafar Husain a posté un exemple d'utilisation des arbres d'expression pour vérifier null dans une chaîne, Runtime macros in C# 3.

Cela a évidemment des implications sur les performances. Maintenant, si seulement nous avions un moyen de le faire au moment de la compilation.

+0

Merci, c'est un excellent lien. – Keith

+1

Je suis d'accord avec votre premier commentaire. Le problème, à mon avis, c'est que ce n'est pas évident d'après le code. C'est moins encombré, mais c'est seulement plus facile à comprendre, une fois que vous savez ce qu'il fait. – xan

5

Je dois juste dire que j'aime ce hack!

Je n'avais pas réalisé que les méthodes d'extension n'impliquaient pas une vérification nulle, mais cela a du sens. Comme James l'a souligné, la méthode d'extension call elle-même n'est pas plus chère qu'une méthode normale, cependant si vous en faites une tonne, alors il est logique de suivre le Null Object Pattern, que ljorquera a suggéré. Ou pour utiliser un objet nul et ?? ensemble.

class Class1 
{ 
    public static readonly Class1 Empty = new Class1(); 
. 
. 
x = (obj1 ?? Class1.Empty).X; 
+0

Les méthodes d'extension en elles-mêmes ne sont pas plus coûteuses, cependant, cette méthode d'extension particulière est plus coûteuse, car elle nécessite une méthode lambda/anonyme. Les Lambdas sont compilés en allocations de classe en arrière-plan. Ainsi, c'est plus cher car cela nécessite une allocation. –

+1

@JudahHimango Les lamdas ne sont conformes aux allocations de classe que si elles capturent une variable (devenant une fermeture). Si ce n'est pas le cas, ils sont transformés en une méthode statique générée par le compilateur ... Vous pouvez le vérifier en regardant une DLL compilée avec quelque chose comme dotPeek (http://www.jetbrains.com/decompiler/) –

1

Pour le lecteur non averti, il semble que vous appeliez une méthode sur une référence nulle. Si vous voulez, je vous suggère de le mettre dans une classe utilitaire plutôt que d'utiliser une méthode d'extension:


propertyValue1 = Util.IfNotNull(myObject1, x => x.StringProperty); 
propertyValue2 = Util.IfNotNull(myObject2, x => x.IntProperty, 0); 

Le « Util. » grilles, mais est OMI le moindre mal syntaxique. De même, si vous développez cela en tant que membre d'une équipe, demandez-lui doucement ce que les autres pensent et font. La cohérence à travers une base de code pour les modèles fréquemment utilisés est importante.

1

Alors que les méthodes d'extension provoquent généralement des malentendus lorsqu'elles sont appelées à partir d'instances nulles, je pense que l'intention est assez simple dans ce cas.

string x = null; 
int len = x.IfNotNull(y => y.Length, 0); 

Je voudrais être sûr que cette méthode statique fonctionne sur les types de valeur qui peuvent être null, comme int?

Edit: compilateur dit qu'aucun de ceux-ci sont valables:

public void Test() 
    { 
     int? x = null; 
     int a = x.IfNotNull(z => z.Value + 1, 3); 
     int b = x.IfNotNull(z => z.Value + 1); 
    } 

Autre que cela, allez-y.

+0

Voilà pourquoi il y a deux surcharges - une qui a besoin d'une valeur par défaut sans contrainte sur le type de résultat, et une qui n'a pas besoin d'une valeur par défaut mais qui contraint le résultat à des types de référence. – Keith

+0

Voir Modifier pour un test. –

+0

C'est parce que int? est en fait compilé à Nullable , qui est en fait une structure. C'est seulement la magie du compilateur qui vous permet de le comparer à un null (il échoue correctement la contrainte where TResult: class). Je pourrais avoir besoin d'ajouter une autre surcharge spécifique à Nullable Keith

15

Voici une autre solution, pour les membres chaînés, y compris les méthodes d'extension:

public static U PropagateNulls<T,U> (this T obj 
            ,Expression<Func<T,U>> expr) 
{ if (obj==null) return default(U); 

    //uses a stack to reverse Member1(Member2(obj)) to obj.Member1.Member2 
    var members = new Stack<MemberInfo>(); 

    bool  searchingForMembers = true; 
    Expression currentExpression = expr.Body; 

    while (searchingForMembers) switch (currentExpression.NodeType) 
    { case ExpressionType.Parameter: searchingForMembers = false; break; 

      case ExpressionType.MemberAccess:  
      { var ma= (MemberExpression) currentExpression; 
      members.Push(ma.Member); 
      currentExpression = ma.Expression;   
      } break;  

      case ExpressionType.Call: 
      { var mc = (MethodCallExpression) currentExpression; 
      members.Push(mc.Method); 

      //only supports 1-arg static methods and 0-arg instance methods 
      if ( (mc.Method.IsStatic && mc.Arguments.Count == 1) 
       || (mc.Arguments.Count == 0)) 
      { currentExpression = mc.Method.IsStatic ? mc.Arguments[0] 
                : mc.Object; 
       break; 
      } 

      throw new NotSupportedException(mc.Method+" is not supported"); 
     } 

     default: throw new NotSupportedException 
         (currentExpression.GetType()+" not supported"); 
    } 

    object currValue = obj; 
    while(members.Count > 0) 
    { var m = members.Pop(); 

     switch(m.MemberType) 
     { case MemberTypes.Field: 
      currValue = ((FieldInfo) m).GetValue(currValue); 
      break; 

     case MemberTypes.Method: 
      var method = (MethodBase) m; 
      currValue = method.IsStatic 
           ? method.Invoke(null,new[]{currValue}) 
           : method.Invoke(currValue,null); 
      break; 

     case MemberTypes.Property: 
      var method = ((PropertyInfo) m).GetGetMethod(true); 
       currValue = method.Invoke(currValue,null); 
      break; 

     }  

     if (currValue==null) return default(U); 
    } 

    return (U) currValue;  
} 

Ensuite, vous pouvez faire cela où tout peut être nulle, ou pas:

foo.PropagateNulls(x => x.ExtensionMethod().Property.Field.Method()); 
+1

Très perspicace. Je vous remercie! –

+0

J'aime cette idée! Cependant, il (ainsi que toutes les autres solutions ici) ne semble pas fonctionner dans une déclaration LINQ. Par exemple lorsque vous faites un .Select dans un nouveau type anonyme comme .Select (s => new {MyNewProperty = s.PropogateNulls (p => p.Thing)}). Cela ne fonctionne pas. Vous devez toujours utiliser l'ancienne vérification null là. –

+0

Vous devez modifier le code pour accepter les méthodes statiques avec deux arguments, pour 'Enumerable.Select (src, lambda)' –

0

Voici une autre solution en utilisant myObject.NullSafe (x => x.SomeProperty.NullSafe (x => x.SomeMethod)), a expliqué à http://www.epitka.blogspot.com/

+0

Merci, mais c'est beaucoup plus de code pour faire la même chose. De plus, votre classe Maybe est très similaire à la classe Nullable du framework et en utilisant Invoke vous ajoutez un hit de performance inutile. Encore - c'est agréable de voir une autre solution au même problème. – Keith

1

Pas une réponse à la question exacte posée, mais il est Opérateur Null-Conditionalin C# 6.0. Je soutiens que ce sera un mauvais choix d'utiliser l'option OP depuis C# 6.0 :)

Ainsi, votre expression est plus simple,

string propertyValue = myObject?.StringProperty; 

En cas myObject est nulle, il renvoie null. Dans le cas où la propriété est un type de valeur que vous devez utiliser équivalent type Nullable, comme,

int? propertyValue = myObject?.IntProperty; 

Ou sinon vous pouvez fusionner avec l'opérateur coalescent null pour donner une valeur par défaut en cas de nul.Pour exemple,

int propertyValue = myObject?.IntProperty ?? 0; 

?. n'est pas la seule syntaxe disponible. Pour les propriétés indexées, vous pouvez utiliser ?[..]. Pour exemple,

string propertyValue = myObject?[index]; //returns null in case myObject is null 

Un comportement surprenant de l'opérateur ?. est qu'il peut contourner intelligemment .Member ultérieurs appels si l'objet se trouve être nulle. Un tel exemple est donné dans le lien:

var result = value?.Substring(0, Math.Min(value.Length, length)).PadRight(length); 

Dans ce cas result est nulle si value est nulle et value.Length expression ne serait pas aboutir à NullReferenceException.

Questions connexes