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


224

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

GetString(
    inputString,
    ref Client.WorkPhone)

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

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

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


Что касается того, почему, посмотрите этот 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);
    }
}

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

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отличным образцом; Жаль, что он не используется больше.


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 в списке аргументов может быть передана только ссылка на переменную. Свойство не квалифицируется как ссылка на переменную и, следовательно, не может быть использовано.


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

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

0

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

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

0

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.