Passer des propriétés par référence en C #

224

J'essaie de faire ce qui suit:

GetString(
    inputString,
    ref Client.WorkPhone)

private void GetString(string inValue, ref string outValue)
{
    if (!string.IsNullOrEmpty(inValue))
    {
        outValue = inValue;
    }
}

Cela me donne une erreur de compilation. Je pense que c'est assez clair ce que j'essaie de réaliser. Fondamentalement, je veux GetStringcopier le contenu d'une chaîne d'entrée dans la WorkPhonepropriété de Client.

Est-il possible de transmettre une propriété par référence?

yogibear
la source
Pour savoir pourquoi, consultez ce stackoverflow.com/questions/564557/…
nawfal

Réponses:

423

Les propriétés ne peuvent pas être transmises par référence. Voici quelques façons de contourner cette limitation.

1. Valeur de retour

string GetString(string input, string output)
{
    if (!string.IsNullOrEmpty(input))
    {
        return input;
    }
    return output;
}

void Main()
{
    var person = new Person();
    person.Name = GetString("test", person.Name);
    Debug.Assert(person.Name == "test");
}

2. Délégué

void GetString(string input, Action<string> setOutput)
{
    if (!string.IsNullOrEmpty(input))
    {
        setOutput(input);
    }
}

void Main()
{
    var person = new Person();
    GetString("test", value => person.Name = value);
    Debug.Assert(person.Name == "test");
}

3. Expression LINQ

void GetString<T>(string input, T target, Expression<Func<T, string>> outExpr)
{
    if (!string.IsNullOrEmpty(input))
    {
        var expr = (MemberExpression) outExpr.Body;
        var prop = (PropertyInfo) expr.Member;
        prop.SetValue(target, input, null);
    }
}

void Main()
{
    var person = new Person();
    GetString("test", person, x => x.Name);
    Debug.Assert(person.Name == "test");
}

4. Réflexion

void GetString(string input, object target, string propertyName)
{
    if (!string.IsNullOrEmpty(input))
    {
        var prop = target.GetType().GetProperty(propertyName);
        prop.SetValue(target, input);
    }
}

void Main()
{
    var person = new Person();
    GetString("test", person, nameof(Person.Name));
    Debug.Assert(person.Name == "test");
}
Nathan Baulch
la source
2
J'adore les exemples. Je trouve que c'est aussi un excellent endroit pour les méthodes d'extension: codechaîne statique publique GetValueOrDefault (cette chaîne s, chaîne isNullString) {if (s == null) {s = isNullString; } Retour; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack
9
Dans la solution 2, le 2e paramètre getOutputn'est pas nécessaire.
Jaider
31
Et je pense qu'un meilleur nom pour la solution 3 est Réflexion.
Jaider
1
Dans la solution 2, le 2ème paramètre getOutput n'est pas nécessaire - vrai mais je l'ai utilisé dans GetString pour voir quelle était la valeur que je définissais. Je ne sais pas comment faire cela sans ce paramètre.
Petras
3
@GoneCodingGoodbye: mais l'approche la moins efficace. Utiliser la réflexion pour simplement attribuer une valeur à une propriété, c'est comme prendre un marteau pour casser un écrou. En outre, une méthode GetStringqui est censée définir une propriété est clairement mal nommée.
Tim Schmelter
27

sans dupliquer la propriété

void Main()
{
    var client = new Client();
    NullSafeSet("test", s => client.Name = s);
    Debug.Assert(person.Name == "test");

    NullSafeSet("", s => client.Name = s);
    Debug.Assert(person.Name == "test");

    NullSafeSet(null, s => client.Name = s);
    Debug.Assert(person.Name == "test");
}

void NullSafeSet(string value, Action<string> setter)
{
    if (!string.IsNullOrEmpty(value))
    {
        setter(value);
    }
}
Firo
la source
4
+1 pour changer le nom GetStringde NullSafeSet, parce que le premier n'a pas de sens ici.
Camilo Martin
25

J'ai écrit un wrapper en utilisant la variante ExpressionTree et c # 7 (si quelqu'un est intéressé):

public class Accessor<T>
{
    private Action<T> Setter;
    private Func<T> Getter;

    public Accessor(Expression<Func<T>> expr)
    {
        var memberExpression = (MemberExpression)expr.Body;
        var instanceExpression = memberExpression.Expression;
        var parameter = Expression.Parameter(typeof(T));

        if (memberExpression.Member is PropertyInfo propertyInfo)
        {
            Setter = Expression.Lambda<Action<T>>(Expression.Call(instanceExpression, propertyInfo.GetSetMethod(), parameter), parameter).Compile();
            Getter = Expression.Lambda<Func<T>>(Expression.Call(instanceExpression, propertyInfo.GetGetMethod())).Compile();
        }
        else if (memberExpression.Member is FieldInfo fieldInfo)
        {
            Setter = Expression.Lambda<Action<T>>(Expression.Assign(memberExpression, parameter), parameter).Compile();
            Getter = Expression.Lambda<Func<T>>(Expression.Field(instanceExpression,fieldInfo)).Compile();
        }

    }

    public void Set(T value) => Setter(value);

    public T Get() => Getter();
}

Et utilisez-le comme:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");
Sven
la source
3
Meilleure réponse ici. Savez-vous quel est l'impact sur les performances? Ce serait bien de l'avoir couvert dans la réponse. Je ne connais pas beaucoup les arborescences d'expression, mais je m'attendrais à ce que l'utilisation de Compile () signifie que l'instance d'accesseur contient en fait du code compilé IL et donc l'utilisation d'un nombre constant d'accesseurs n fois serait acceptable, mais en utilisant au total n accesseurs ( coût élevé du ctor) ne le serait pas.
mancze
Excellent code! À mon avis, c'est la meilleure réponse. Le plus générique. Comme le dit mancze ... Cela devrait avoir un impact énorme sur les performances et ne devrait être utilisé que dans un contexte où la clarté du code est plus importante que la performance.
Eric Ouellet
5

Si vous souhaitez obtenir et définir la propriété à la fois, vous pouvez l'utiliser en C # 7:

GetString(
    inputString,
    (() => client.WorkPhone, x => client.WorkPhone = x))

void GetString(string inValue, (Func<string> get, Action<string> set) outValue)
{
    if (!string.IsNullOrEmpty(outValue))
    {
        outValue.set(inValue);
    }
}
Pastille
la source
3

Une autre astuce non encore mentionnée est que la classe qui implémente une propriété (par exemple Foode type Bar) définisse également un délégué delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);et implémente une méthode ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(et éventuellement des versions pour deux et trois "paramètres supplémentaires") qui passera sa représentation interne de Fooà la procédure fournie comme refparamètre. Cela présente quelques grands avantages par rapport aux autres méthodes de travail avec la propriété:

  1. La propriété est mise à jour "sur place"; si la propriété est d'un type compatible avec les méthodes `Interlocked`, ou si c'est une structure avec des champs exposés de ces types, les méthodes` Interlocked` peuvent être utilisées pour effectuer des mises à jour atomiques de la propriété.
  2. Si la propriété est une structure à champ exposé, les champs de la structure peuvent être modifiés sans avoir à en faire des copies redondantes.
  3. Si la méthode `ActByRef` transmet un ou plusieurs paramètres` ref` de son appelant au délégué fourni, il peut être possible d'utiliser un délégué singleton ou statique, évitant ainsi la nécessité de créer des fermetures ou des délégués au moment de l'exécution.
  4. L'établissement sait quand il est "travaillé avec". Bien qu'il soit toujours nécessaire de faire preuve de prudence lors de l'exécution de code externe tout en maintenant un verrou, si l'on peut faire confiance aux appelants pour ne rien faire de plus dans leur rappel qui pourrait nécessiter un autre verrou, il peut être pratique d'avoir la méthode qui garde l'accès à la propriété avec un verrouiller, de sorte que les mises à jour qui ne sont pas compatibles avec `CompareExchange` pourraient toujours être effectuées de manière quasi atomique.

Faire passer les choses refest un excellent modèle; dommage qu'il ne soit plus utilisé.

supercat
la source
3

Juste une petite extension de la solution Linq Expression de Nathan . Utilisez plusieurs paramètres génériques pour que la propriété ne se limite pas à la chaîne.

void GetString<TClass, TProperty>(string input, TClass outObj, Expression<Func<TClass, TProperty>> outExpr)
{
    if (!string.IsNullOrEmpty(input))
    {
        var expr = (MemberExpression) outExpr.Body;
        var prop = (PropertyInfo) expr.Member;
        if (!prop.GetValue(outObj).Equals(input))
        {
            prop.SetValue(outObj, input, null);
        }
    }
}
Zick Zhang
la source
2

Ceci est couvert dans la section 7.4.1 de la spécification du langage C #. Seule une référence de variable peut être passée en tant que paramètre ref ou out dans une liste d'arguments. Une propriété ne peut pas être considérée comme une référence de variable et ne peut donc pas être utilisée.

JaredPar
la source
2

Ce n'est pas possible. Tu pourrais dire

Client.WorkPhone = GetString(inputString, Client.WorkPhone);

où se WorkPhonetrouve une stringpropriété inscriptible et la définition de GetStringest modifiée en

private string GetString(string input, string current) { 
    if (!string.IsNullOrEmpty(input)) {
        return input;
    }
    return current;
}

Cela aura la même sémantique que vous semblez essayer.

Ce n'est pas possible car une propriété est en réalité une paire de méthodes déguisées. Chaque propriété met à disposition des getters et setters accessibles via une syntaxe de type champ. Lorsque vous essayez d'appeler GetStringcomme vous l'avez proposé, ce que vous transmettez est une valeur et non une variable. La valeur que vous transmettez est celle renvoyée par le getter get_WorkPhone.

Jason
la source
1

Ce que vous pourriez essayer de faire est de créer un objet pour contenir la valeur de la propriété. De cette façon, vous pourriez passer l'objet et avoir toujours accès à la propriété à l'intérieur.

Anthony Reese
la source
1

Les propriétés ne peuvent pas être transmises par référence? Faites-en ensuite un champ et utilisez la propriété pour le référencer publiquement:

public class MyClass
{
    public class MyStuff
    {
        string foo { get; set; }
    }

    private ObservableCollection<MyStuff> _collection;

    public ObservableCollection<MyStuff> Items { get { return _collection; } }

    public MyClass()
    {
        _collection = new ObservableCollection<MyStuff>();
        this.LoadMyCollectionByRef<MyStuff>(ref _collection);
    }

    public void LoadMyCollectionByRef<T>(ref ObservableCollection<T> objects_collection)
    {
        // Load refered collection
    }
}
macedo123
la source
0

Vous ne pouvez pas refune propriété, mais si vos fonctions ont besoin des deux getet d'un setaccès, vous pouvez passer autour d'une instance d'une classe avec une propriété définie:

public class Property<T>
{
    public delegate T Get();
    public delegate void Set(T value);
    private Get get;
    private Set set;
    public T Value {
        get {
            return get();
        }
        set {
            set(value);
        }
    }
    public Property(Get get, Set set) {
        this.get = get;
        this.set = set;
    }
}

Exemple:

class Client
{
    private string workPhone; // this could still be a public property if desired
    public readonly Property<string> WorkPhone; // this could be created outside Client if using a regular public property
    public int AreaCode { get; set; }
    public Client() {
        WorkPhone = new Property<string>(
            delegate () { return workPhone; },
            delegate (string value) { workPhone = value; });
    }
}
class Usage
{
    public void PrependAreaCode(Property<string> phone, int areaCode) {
        phone.Value = areaCode.ToString() + "-" + phone.Value;
    }
    public void PrepareClientInfo(Client client) {
        PrependAreaCode(client.WorkPhone, client.AreaCode);
    }
}
chess123mate
la source
0

La réponse acceptée est bonne si cette fonction est dans votre code et que vous pouvez la modifier. Mais parfois, vous devez utiliser un objet et une fonction d'une bibliothèque externe et vous ne pouvez pas modifier la définition de la propriété et de la fonction. Ensuite, vous pouvez simplement utiliser une variable temporaire.

var phone = Client.WorkPhone;
GetString(input, ref phone);
Client.WorkPhone = phone;
palota
la source
0

Pour voter sur cette question, voici une suggestion active de la façon dont cela pourrait être ajouté à la langue. Je ne dis pas que c'est la meilleure façon de le faire (du tout), n'hésitez pas à présenter votre propre suggestion. Mais permettre aux propriétés d'être passées par ref comme Visual Basic peut déjà le faire aiderait énormément à simplifier du code, et assez souvent!

https://github.com/dotnet/csharplang/issues/1235

Nicholas Petersen
la source