Passare le proprietà per riferimento in C #


224

Sto provando a fare quanto segue:

GetString(
    inputString,
    ref Client.WorkPhone)

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

Questo mi sta dando un errore di compilazione. Penso che sia abbastanza chiaro cosa sto cercando di ottenere. Fondamentalmente voglio GetStringcopiare il contenuto di una stringa di input nella WorkPhoneproprietà di Client.

È possibile passare una proprietà per riferimento?


Quanto al perché, vedere questo stackoverflow.com/questions/564557/...
Nawfal

Risposte:


423

Le proprietà non possono essere passate per riferimento. Ecco alcuni modi per aggirare questa limitazione.

1. Valore di ritorno

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. Delegato

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. Espressione 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. Riflessione

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");
}

2
Adoro gli esempi. Trovo che questo sia un ottimo posto anche per i metodi di estensione: codestringa statica pubblica GetValueOrDefault (questa stringa s, stringa isNullString) {if (s == null) {s = isNullString; } ritorna; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
Blackjacket:

9
Nella soluzione 2, il secondo parametro getOutputnon è necessario.
Jaider,

31
E penso che un nome migliore per la soluzione 3 sia Reflection.
Jaider,

1
Nella soluzione 2, il secondo parametro getOutput non è necessario - vero ma l'ho usato all'interno di GetString per vedere quale fosse il valore che stavo impostando. Non sono sicuro di come farlo senza questo parametro.
Petras,

3
@GoneCodingGoodbye: ma l'approccio meno efficiente. Usare la riflessione per assegnare semplicemente un valore a una proprietà è come prendere una mazza per rompere un dado. Inoltre, un metodo GetStringche dovrebbe impostare una proprietà è chiaramente errato.
Tim Schmelter,

27

senza duplicare la proprietà

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);
    }
}

4
+1 per cambiare il nome GetStringin NullSafeSet, perché il primo non ha senso qui.
Camilo Martin,

25

Ho scritto un wrapper usando la variante ExpressionTree ec # 7 (se qualcuno è interessato):

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();
}

E usalo come:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");

3
La migliore risposta qui. Sai qual è l'impatto sulle prestazioni? Sarebbe bello averlo coperto nella risposta. Non ho molta familiarità con gli alberi delle espressioni, ma mi aspetto che l'uso di Compile () significhi che l'istanza dell'accessor contiene effettivamente codice compilato IL e che quindi l'utilizzo di un numero costante di accessi n-volte andrebbe bene, ma utilizzando un totale di n accessori ( alto costo del ctor) no.
mancze,

Ottimo codice! La mia opinione è la migliore risposta. Il più generico. Come dice mancze ... Dovrebbe avere un impatto enorme sulle prestazioni e dovrebbe essere usato solo in un contesto in cui la chiarezza del codice è più importante della performance.
Eric Ouellet,

5

Se si desidera ottenere e impostare entrambe le proprietà, è possibile utilizzarlo in 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);
    }
}

3

Un altro trucco non ancora menzionato è che la classe che implementa una proprietà (ad es. Di Footipo Bar) definisce anche un delegato delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);e implementa un metodo ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(e possibilmente anche versioni per due e tre "parametri extra") che passerà la sua rappresentazione interna di Fooa la procedura fornita come refparametro. Questo ha un paio di grandi vantaggi rispetto ad altri metodi di lavoro con la proprietà:

  1. La proprietà viene aggiornata "sul posto"; se la proprietà è di un tipo compatibile con i metodi `Interlocked` o se è una struttura con campi esposti di tali tipi, i metodi` Interlocked` possono essere usati per eseguire aggiornamenti atomici sulla proprietà.
  2. Se la proprietà è una struttura di campo esposto, i campi della struttura possono essere modificati senza doverne fare copie ridondanti.
  3. Se il metodo `ActByRef` passa uno o più parametri` ref` dal chiamante al delegato fornito, potrebbe essere possibile utilizzare un delegato singleton o statico, evitando così la necessità di creare chiusure o delegati in fase di esecuzione.
  4. La proprietà sa quando viene "lavorato". Mentre è sempre necessario usare cautela nell'esecuzione di un codice esterno mentre si tiene un blocco, se uno può fidarsi dei chiamanti di non fare troppo nel loro callback che potrebbe richiedere un altro blocco, può essere pratico avere il metodo di proteggere l'accesso alla proprietà con un lock, in modo tale che gli aggiornamenti che non sono compatibili con `CompareExchange` possano ancora essere eseguiti quasi atomicamente.

Passare le cose refè un modello eccellente; peccato che non sia più utilizzato.


3

Solo una piccola espansione alla soluzione Linq Expression di Nathan . Utilizzare parametri multi generici in modo che la proprietà non si limiti alla stringa.

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);
        }
    }
}

2

Questo è trattato nella sezione 7.4.1 delle specifiche del linguaggio C #. Solo un riferimento a variabile può essere passato come parametro ref o out in un elenco di argomenti. Una proprietà non si qualifica come riferimento variabile e quindi non può essere utilizzata.


2

Non è possibile. Potresti dire

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

dove WorkPhoneè una stringproprietà scrivibile e la definizione di GetStringè cambiata in

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

Questo avrà la stessa semantica che sembra stia provando.

Questo non è possibile perché una proprietà è in realtà una coppia di metodi mascherati. Ogni proprietà rende disponibili getter e setter accessibili tramite una sintassi simile a un campo. Quando tenti di chiamare GetStringcome da te proposto, ciò che stai passando è un valore e non una variabile. Il valore che stai passando è quello restituito dal getter get_WorkPhone.


1

Quello che potresti provare a fare è creare un oggetto per contenere il valore della proprietà. In questo modo potresti passare l'oggetto e avere comunque accesso alla proprietà all'interno.


1

Le proprietà non possono essere passate per riferimento? Renderlo quindi un campo e utilizzare la proprietà per fare riferimento a esso pubblicamente:

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
    }
}

0

Non puoi refuna proprietà, ma se le tue funzioni richiedono entrambe gete l' setaccesso puoi passare un'istanza di una classe con una proprietà definita:

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;
    }
}

Esempio:

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);
    }
}

0

La risposta accettata è buona se quella funzione è nel tuo codice e puoi modificarla. Ma a volte devi usare un oggetto e una funzione da qualche libreria esterna e non puoi cambiare la definizione di proprietà e funzione. Quindi puoi semplicemente usare una variabile temporanea.

var phone = Client.WorkPhone;
GetString(input, ref phone);
Client.WorkPhone = phone;

0

Per votare su questo tema, ecco un suggerimento attivo su come questo potrebbe essere aggiunto alla lingua. Non sto dicendo che questo è il modo migliore per farlo (affatto), sentiti libero di dare il tuo suggerimento. Ma consentire alle proprietà di essere passate da ref come già può fare Visual Basic aiuterebbe enormemente a semplificare un po 'di codice e abbastanza spesso!

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.