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

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

GetString(
    inputString,
    ref Client.WorkPhone)

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

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

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

Ответ 1

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

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

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))
    {
        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

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

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

Ответ 3

Я написал обертку, используя вариант ExpressionTree и С# 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");

Ответ 4

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

Ответ 5

Еще один трюк, о котором еще не упоминалось, заключается в том, чтобы класс, который реализует свойство (например, 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. Свойство обновляется "на месте"; если свойство имеет тип, совместимый с методами "блокировки", или если это структура с открытыми полями таких типов, методы "блокировки" могут использоваться для выполнения атомных обновлений для свойства.
  2. Если свойство представляет собой структуру открытого поля, поля структуры могут быть изменены без необходимости делать какие-либо избыточные копии.
  3. Если метод `ActByRef` передает один или несколько параметров` ref` через своего вызывающего абонента в предоставленный делегат, может быть возможно использовать однотонный или статический делегат, что позволяет избежать необходимости создавать закрытие или делегаты при запуске, время.
  4. Имущество знает, когда оно "работает". Хотя всегда необходимо использовать осторожность при выполнении внешнего кода, удерживая блокировку, если кто-то может доверять вызывающим абонентам, чтобы они не делали ничего в своем обратном вызове, что может потребовать другой блокировки, может быть целесообразным, чтобы метод защищал доступ к свойствам с помощью блокировки, так что обновления, которые не совместимы с `CompareExchange`, могут выполняться квазиатомно.

    Передача вещей ref - отличный образец; слишком плохо он не использовал больше.

Ответ 6

Просто небольшое расширение для решения для решения Nathan Linq Expression. Используйте множественный общий параметр, чтобы свойство не ограничивалось строкой.

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

Ответ 7

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

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

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

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

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

Вы не можете 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);
    }
}

Ответ 12

Также ответил Pellet, для потокобезопасного доступа к свойствам:

public class SomeClass
{
    private DateTime _connectionTime;
    private string _userName;

    private void Set<T>(T value, (Func<T> get, Action<T> set) property)
    {
        bool acquiredLock = false;

        Monitor.Enter(property.get(), ref acquiredLock);
        try
        {
            property.set(value);
        }
        finally
        {
            if (acquiredLock)
                Monitor.Exit(property.get());
        }
    }

    public DateTime ConnectionTime
    {
        get => _connectionTime;
        set => Set(value, (() => _connectionTime, val => _connectionTime = val));
    }

    public string UserName
    {
        get => _userName;
        set => Set(value, (() => _userName, val => _userName = val));
    }
}

Ответ 13

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

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