简体   繁体   中英

Passing properties by reference in C#

I'm trying to do do the following:

GetString(
    inputString,
    ref Client.WorkPhone)

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

This is giving me a compile error. I think its pretty clear what I'm trying to achieve. Basically I want GetString to copy the contents of an input string to the WorkPhone property of Client .

Is it possible to pass a property by reference?

Properties cannot be passed by reference. Here are a few ways you can work around this limitation.

1. Return Value

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

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

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

without duplicating the property

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

I wrote a wrapper using the ExpressionTree variant and c#7 (if somebody is interested):

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

And use it like:

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

If you want to get and set the property both, you can use this 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);
    }
}

Just a little expansion to Nathan's Linq Expression solution . Use multi generic param so that the property doesn't limited to string.

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

Another trick not yet mentioned is to have the class which implements a property (eg Foo of type Bar ) also define a delegate delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2); and implement a method ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1) (and possibly versions for two and three "extra parameters" as well) which will pass its internal representation of Foo to the supplied procedure as a ref parameter. This has a couple of big advantages over other methods of working with the property:

  1. The property is updated "in place"; if the property is of a type that's compatible with `Interlocked` methods, or if it is a struct with exposed fields of such types, the `Interlocked` methods may be used to perform atomic updates to the property.
  2. If the property is an exposed-field structure, the fields of the structure may be modified without having to make any redundant copies of it.
  3. If the `ActByRef` method passes one or more `ref` parameters through from its caller to the supplied delegate, it may be possible to use a singleton or static delegate, thus avoiding the need to create closures or delegates at run-time.
  4. The property knows when it is being "worked with". While it is always necessary to use caution executing external code while holding a lock, if one can trust callers not to do too do anything in their callback that might require another lock, it may be practical to have the method guard the property access with a lock, such that updates which aren't compatible with `CompareExchange` could still be performed quasi-atomically.

Passing things be ref is an excellent pattern; too bad it's not used more.

This is covered in section 7.4.1 of the C# language spec. Only a variable-reference can be passed as a ref or out parameter in an argument list. A property does not qualify as a variable reference and hence cannot be used.

This is not possible. You could say

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

where WorkPhone is a writeable string property and the definition of GetString is changed to

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

This will have the same semantics that you seem to be trying for.

This isn't possible because a property is really a pair of methods in disguise. Each property makes available getters and setters that are accessible via field-like syntax. When you attempt to call GetString as you've proposed, what you're passing in is a value and not a variable. The value that you are passing in is that returned from the getter get_WorkPhone .

Properties cannot be passed by reference ? Make it a field then, and use the property to reference it publicly:

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

What you could try to do is create an object to hold the property value. That way you could pass the object and still have access to the property inside.

You can't ref a property, but if your functions need both get and set access you can pass around an instance of a class with a property defined:

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

Example:

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

The accepted answer is good if that function is in your code and you can modify it. But sometimes you have to use an object and a function from some external library and you can't change the property and function definition. Then you can just use a temporary variable.

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

To vote on this issue, here is one active suggestion of how this could be added to the language. I'm not saying this is the best way to do this (at all), feel free to put out your own suggestion. But allowing properties to be passed by ref like Visual Basic already can do would hugely help simplify some code, and quite often!

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

It seems that you are needing to impose a business rule constraint on that field, while at the same time wanting to keep your code as DRY as possible.

It is achievable and also preserves your domain semantics by implementing a full property on that field and using your re-usable method:

public class Client
{
    private string workPhone;

    public string WorkPhone
    {
        get => workPhone;
        set => SafeSetString(ref workPhone, value);
    }

    private void SafeSetString(ref string target, string source)
    {
        if (!string.IsNullOrEmpty(source))
        {
            target = source;
        }
    }
}

The SafeSetString method can be placed in a Utilities class or wherever it makes sense.

Yes, you can't pass a property but you can convert your property to a property with backing field and do something like this.

public class SomeClass 
{
  private List<int> _myList;
  public List<int> MyList
  { 
    get => return _myList;
    set => _myList = value;
  }
  public ref List<int> GetMyListByRef()
  {
    return ref _myList;
  }
}

but there are better solutions like action delegate etc.

Inspired by Sven's expression tree solution , below is a smplified version that doesn't rely on reflection. Also, it removes the unnecessary custom getter and field expressions.

using System;
using System.Linq.Expressions;

namespace Utils;

public class Accessor<T>
{
    public Accessor(Expression<Func<T>> expression)
    {
        if (expression.Body is not MemberExpression memberExpression)
            throw new ArgumentException("expression must be return a field or property");
        var parameterExpression = Expression.Parameter(typeof(T));

        _setter = Expression.Lambda<Action<T>>(Expression.Assign(memberExpression, parameterExpression), parameterExpression).Compile();
        _getter = expression.Compile();
    }

    public void Set(T value) => _setter(value);
    public T Get() => _getter();

    private readonly Action<T> _setter;
    private readonly Func<T> _getter;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM