Передача свойств по ссылке в C #

224

Я пытаюсь сделать следующее:

GetString(
    inputString,
    ref Client.WorkPhone)

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

Это дает мне ошибку компиляции. Я думаю, довольно ясно, чего я пытаюсь достичь. В основном я хочу GetStringскопировать содержимое входной строки в WorkPhoneсвойство Client.

Можно ли передать недвижимость по ссылке?

yogibear
источник
Что касается того, почему, посмотрите этот stackoverflow.com/questions/564557/…
nawfal

Ответы:

423

Свойства не могут быть переданы по ссылке. Вот несколько способов обойти это ограничение.

1. Возвращаемое значение

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. Делегат

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. LINQ Expression

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. Отражение

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
Люблю примеры. Я считаю, что это отличное место и для методов расширения: codeпубличная статическая строка GetValueOrDefault (эта строка s, строка isNullString) {if (s == null) {s = isNullString; } вернуть s; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack
9
В решении 2 второй параметр getOutputне нужен.
Jaider
31
И я думаю, что лучшее решение для решения 3 - «Отражение».
Jaider
1
В решении 2 второй параметр getOutput не нужен - правда, но я использовал его внутри GetString, чтобы увидеть, какое значение я задавал. Не уверен, как это сделать без этого параметра.
Петрас
3
@GoneCodingGoodbye: но наименее эффективный подход. Использование отражения для простого присваивания значения свойству похоже на кувалду, чтобы сломать орех. Кроме того, метод, GetStringкоторый должен установить свойство, явно неправильно назван.
Тим Шмельтер,
27

без дублирования собственности

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
источник
4
+1 за изменение имени GetStringна NullSafeSet, потому что первое здесь не имеет смысла.
Камило Мартин
25

Я написал обертку, используя вариант ExpressionTree и c # 7 (если кому-то интересно):

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

И используйте это как:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");
Sven
источник
3
Лучший ответ здесь. Знаете ли вы, что влияет на производительность? Было бы хорошо, чтобы это было включено в ответ. Я не очень знаком с деревьями выражений, но я ожидаю, что использование Compile () означает, что экземпляр средства доступа содержит фактически скомпилированный код IL, и поэтому использование постоянного числа средств доступа n-раз будет нормально, но с использованием всего n средств доступа ( высокая стоимость ктор) не будет.
Манч
Отличный код! Мое мнение, это лучший ответ. Самый общий. Как говорит mancze ... Это должно оказать огромное влияние на производительность и должно использоваться только в контексте, где ясность кода важнее, чем производительность.
Эрик Оуэлл
5

Если вы хотите получить и установить свойство как, вы можете использовать это в 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

Еще одна хитрость, которая еще не упомянута, заключается в том, чтобы класс, который реализует свойство (например, Fooтипа Bar), также определял делегат delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);и реализовывал метод ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(и, возможно, версии для двух и трех «дополнительных параметров»), который передаст свое внутреннее представление Fooв предоставленная процедура какref параметра. Это имеет пару больших преимуществ перед другими методами работы со свойством:

  1. Свойство обновляется "на месте"; если свойство имеет тип, который совместим с методами Interlocked, или если это структура с открытыми полями таких типов, методы Interlocked могут использоваться для выполнения атомарных обновлений свойства.
  2. Если свойство является структурой открытого поля, поля структуры могут быть изменены без необходимости делать какие-либо избыточные копии этого свойства.
  3. Если метод ActByRef передает один или несколько параметров ref от своего вызывающего к предоставленному делегату, может быть возможно использовать одноэлементный или статический делегат, таким образом избегая необходимости создавать замыкания или делегаты во время выполнения.
  4. Свойство знает, когда с ним «работают». Хотя всегда необходимо соблюдать осторожность при выполнении внешнего кода, удерживая блокировку, если можно доверять вызывающим сторонам, чтобы они не делали в своем обратном вызове ничего, что могло бы потребовать другой блокировки, может оказаться целесообразным, чтобы метод защищал доступ к свойству с помощью блокировка, чтобы обновления, не совместимые с `CompareExchange`, могли выполняться квазиатомно.

Проходящие вещи быть refотличным образцом; Жаль, что он не используется больше.

Supercat
источник
3

Просто небольшое расширение к решению Натана Linq Expression . Используйте универсальный параметр multi, чтобы свойство не ограничивалось строкой.

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

Это описано в разделе 7.4.1 спецификации языка C #. В качестве параметра ref или out в списке аргументов может быть передана только ссылка на переменную. Свойство не квалифицируется как ссылка на переменную и, следовательно, не может быть использовано.

JaredPar
источник
2

Это невозможно. Ты мог бы сказать

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

где свойство WorkPhoneдоступно для записи stringи определение GetStringизменено на

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

Это будет иметь ту же семантику, которую вы, похоже, пытаетесь найти.

Это невозможно, потому что свойство - это действительно замаскированная пара методов. Каждое свойство делает доступными методы получения и установки, доступные через полевой синтаксис. Когда вы пытаетесь позвонитьGetString пытаетесь как вы предложили, вы передаете значение, а не переменную. Значение, которое вы передаете, - это то, что возвращается из получателя get_WorkPhone.

Ясон
источник
1

То, что вы можете попытаться сделать, это создать объект для хранения значения свойства. Таким образом, вы можете передать объект и по-прежнему иметь доступ к свойству внутри.

Энтони Риз
источник
1

Свойства не могут быть переданы по ссылке? Затем сделайте это поле и используйте свойство для публичной ссылки на него:

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
источник
0

Вы не можете refиметь свойство, но если вашим функциям нужны оба getи setдоступ, вы можете передать экземпляр класса с определенным свойством:

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

Пример:

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
источник
0

Принятый ответ хорош, если эта функция есть в вашем коде, и вы можете ее изменить. Но иногда вам приходится использовать объект и функцию из какой-то внешней библиотеки, и вы не можете изменить определение свойства и функции. Тогда вы можете просто использовать временную переменную.

var phone = Client.WorkPhone;
GetString(input, ref phone);
Client.WorkPhone = phone;
Palota
источник
0

Чтобы проголосовать по этому вопросу, вот одно активное предложение о том, как это можно добавить к языку. Я не говорю, что это лучший способ сделать это (вообще), не стесняйтесь выдвигать свое собственное предложение. Но разрешение свойств, передаваемых ref как Visual Basic, уже может сделать, очень помогло бы упростить некоторый код, и довольно часто!

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

Николас Петерсен
источник