Przekazywanie właściwości przez referencję w języku C #

224

Próbuję wykonać następujące czynności:

GetString(
    inputString,
    ref Client.WorkPhone)

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

To daje mi błąd kompilacji. Myślę, że to całkiem jasne, co próbuję osiągnąć. Zasadniczo chcę GetStringskopiować zawartość ciągu wejściowego do WorkPhonewłaściwości Client.

Czy można przekazać właściwość przez referencję?

Miś Yogi
źródło

Odpowiedzi:

423

Właściwości nie mogą być przekazywane przez odwołanie. Oto kilka sposobów obejścia tego ograniczenia.

1. Wartość zwracana

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

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. Wyrażenie 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. Refleksja

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
źródło
2
Uwielbiam przykłady. Uważam, że jest to również świetne miejsce dla metod rozszerzenia: codepubliczny ciąg statyczny GetValueOrDefault (ten ciąg s, ciąg isNullString) {if (s == null) {s = isNullString; } zwroty; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack
9
W rozwiązaniu 2 drugi parametr getOutputjest niepotrzebny.
Jaider
31
Myślę, że lepszą nazwą dla rozwiązania 3 jest Odbicie.
Jaider
1
W rozwiązaniu 2 drugi parametr getOutput jest niepotrzebny - prawda, ale użyłem go w GetString, aby zobaczyć, jaką wartość ustawiłem. Nie jestem pewien, jak to zrobić bez tego parametru.
Petras
3
@GoneCodingGoodbye: ale najmniej efektywne podejście. Używanie odbicia w celu zwykłego przypisania wartości do właściwości jest jak wzięcie młota do zgryzienia orzecha. Również metoda, GetStringktóra ma ustawić właściwość, jest wyraźnie źle nazwana.
Tim Schmelter
27

bez powielania właściwości

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
źródło
4
+1 za zmianę nazwy GetStringna NullSafeSet, ponieważ ta pierwsza nie ma tutaj sensu.
Camilo Martin
25

Napisałem opakowanie używając wariantu ExpressionTree ic # 7 (jeśli ktoś jest zainteresowany):

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

I używaj go w następujący sposób:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");
Sven
źródło
3
Najlepsza odpowiedź tutaj. Czy wiesz, jaki jest wpływ na wydajność? Byłoby miło mieć to ujęte w odpowiedzi. Nie znam się zbytnio na drzewach wyrażeń, ale spodziewałbym się, że użycie Compile () oznacza, że ​​instancja akcesora zawiera kod skompilowany przez IL, a zatem użycie stałej liczby akcesorów n-razy byłoby w porządku, ale użycie łącznie n akcesorów ( wysoki koszt ctor) nie.
mancze
Świetny kod! Moim zdaniem, to najlepsza odpowiedź. Najbardziej ogólny. Jak mówi mancze ... Powinno to mieć ogromny wpływ na wydajność i powinno być używane tylko w kontekście, w którym przejrzystość kodu jest ważniejsza niż wydajność.
Eric Ouellet
5

Jeśli chcesz uzyskać i ustawić właściwość zarówno, możesz użyć tego w 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);
    }
}
Śrut
źródło
3

Inną sztuczką, o której jeszcze nie wspomniano, jest to, aby klasa, która implementuje właściwość (np. FooTypu Bar), również zdefiniowała delegata delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);i zaimplementowała metodę ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(ewentualnie wersje również dla dwóch i trzech „dodatkowych parametrów”), która przekaże swoją wewnętrzną reprezentację Foodo dostarczona procedura jako refparametr. Ma to kilka dużych zalet w porównaniu z innymi metodami pracy z nieruchomością:

  1. Właściwość jest aktualizowana „na miejscu”; jeśli właściwość jest typu zgodnego z metodami „Interlocked” lub jest strukturą z odsłoniętymi polami tego typu, metody „Interlocked” można użyć do przeprowadzenia atomowych aktualizacji właściwości.
  2. Jeśli właściwość jest strukturą pola odsłoniętego, pola struktury mogą być modyfikowane bez konieczności tworzenia zbędnych kopii.
  3. Jeśli metoda `ActByRef` przekazuje jeden lub więcej parametrów` ref` przez swojego wywołującego do dostarczonego delegata, może być możliwe użycie singletona lub statycznego delegata, unikając w ten sposób potrzeby tworzenia zamknięć lub delegatów w czasie wykonywania.
  4. Właściwość wie, kiedy jest „obsługiwana”. Chociaż zawsze należy zachować ostrożność podczas wykonywania kodu zewnętrznego, trzymając blokadę, jeśli można zaufać, że dzwoniący nie zrobią nic w ich wywołaniu zwrotnym, co może wymagać kolejnej blokady, może być praktyczne, aby metoda chroniła dostęp do właściwości za pomocą blokada, tak że aktualizacje, które nie są kompatybilne z programem „CompareExchange”, mogą być nadal wykonywane quasi-atomowo.

Przekazywanie rzeczy refjest doskonałym wzorcem; szkoda, że ​​nie jest więcej używany.

supercat
źródło
3

Tylko małe rozszerzenie do rozwiązania Nathan Linq Expression . Użyj wielu ogólnych parametrów, aby właściwość nie ograniczała się do łańcucha.

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
źródło
2

Jest to omówione w sekcji 7.4.1 specyfikacji języka C #. Tylko odwołanie do zmiennej można przekazać jako parametr ref lub out na liście argumentów. Właściwość nie kwalifikuje się jako odwołanie do zmiennej i dlatego nie można jej użyć.

JaredPar
źródło
2

To jest niemożliwe. Można powiedzieć

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

gdzie WorkPhonejest stringwłaściwość do zapisu i zmieniono definicję GetStringna

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

Będzie to miało tę samą semantykę, o którą się starasz.

Nie jest to możliwe, ponieważ właściwość jest naprawdę parą ukrytych metod. Każda właściwość udostępnia obiekty pobierające i ustawiające, które są dostępne poprzez składnię polową. Gdy próbujesz zadzwonić GetStringzgodnie z propozycją, przekazujesz wartość, a nie zmienną. Przekazywana wartość to wartość zwracana przez moduł pobierający get_WorkPhone.

Jason
źródło
1

Możesz spróbować utworzyć obiekt, w którym będzie przechowywana wartość właściwości. W ten sposób możesz przekazać obiekt i nadal mieć dostęp do nieruchomości w środku.

Anthony Reese
źródło
1

Właściwości nie mogą być przekazywane przez odwołanie? Uczyń go więc polem i użyj właściwości, aby odwoływać się do niego publicznie:

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
źródło
0

Nie możesz mieć refwłaściwości, ale jeśli twoje funkcje potrzebują obu geti setdostępu, możesz przekazać instancję klasy ze zdefiniowaną właściwością:

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

Przykład:

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
źródło
0

Przyjęta odpowiedź jest dobra, jeśli ta funkcja znajduje się w kodzie i można ją zmodyfikować. Ale czasami trzeba użyć obiektu i funkcji z jakiejś zewnętrznej biblioteki i nie można zmienić definicji właściwości i funkcji. Następnie możesz po prostu użyć zmiennej tymczasowej.

var phone = Client.WorkPhone;
GetString(input, ref phone);
Client.WorkPhone = phone;
palota
źródło
0

Aby głosować w tej sprawie, oto jedna aktywna sugestia, w jaki sposób można ją dodać do języka. Nie twierdzę, że jest to najlepszy sposób na zrobienie tego (w ogóle), zachęcamy do przedstawienia własnych sugestii. Ale zezwolenie na przekazywanie właściwości przez referencje, jak to już robi Visual Basic, znacznie pomogłoby uprościć niektóre kody i dość często!

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

Nicholas Petersen
źródło