在C#中通过引用传递属性

Passing properties by reference in C#

我正在尝试执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
GetString(
    inputString,
    ref Client.WorkPhone)

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

这给了我一个编译错误。我想我想要达到的目标很清楚。基本上,我希望GetString将输入字符串的内容复制到ClientWorkPhone属性。

是否可以通过引用传递属性?


属性不能通过引用传递。下面是一些解决这个限制的方法。

1。返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。代表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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");
}

三。LINQ表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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。反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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");
}


不复制属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
    }
}


我用expressionTree变体和c 7(如果有人感兴趣)编写了一个包装器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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();
}

使用方法如下:

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


如果要同时获取和设置属性,可以在C 7中使用:

1
2
3
4
5
6
7
8
9
10
11
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);
    }
}

只是对Nathan的Linq表达式解决方案进行了一点扩展。使用多个通用参数,以便属性不限于字符串。

1
2
3
4
5
6
7
8
9
10
11
12
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);
        }
    }
}

另一个尚未提到的技巧是让实现某个属性的类(例如,Bar类型的Foo也定义一个委托delegate void ActByRef(ref T1 p1, ref T2 p2);并实现一个方法ActOnFoo(ref Bar it, ActByRef proc, ref TX1 extraParam1)(可能还有两个和三个"额外参数"的版本),它将Foo的内部表示传递给所提供的过程。dure作为一个ref参数。与使用该物业的其他方法相比,这有两大优势:

  • "就地"更新属性;如果属性的类型与"interlocked"方法兼容,或者如果它是具有此类类型的公开字段的结构,"interlocked"方法可用于对属性执行原子更新。
  • 如果属性是公开的字段结构,则可以修改结构的字段,而不必对其进行任何冗余复制。
  • 如果"actbyref"方法将一个或多个"ref"参数从其调用方传递给提供的委托,则可以使用单实例委托或静态委托,从而避免在运行时创建闭包或委托。
  • 该物业知道它何时被"使用"。尽管在保持锁的同时执行外部代码总是需要谨慎的,但是如果可以信任调用方,不要在回调中做可能需要另一个锁的任何事情,则使用锁保护属性访问可能是可行的,这样,与"compareexchange"不兼容的更新仍然可以准原子性的。

    以东传1〔10〕是一个很好的模式,可惜没有更多的使用。


    这在C语言规范的第7.4.1节中有介绍。只有变量引用可以作为参数列表中的引用或out参数传递。属性不限定为变量引用,因此无法使用。


    这是不可能的。你可以说

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

    其中WorkPhone为可写string属性,GetString的定义改为

    1
    2
    3
    4
    5
    6
    private string GetString(string input, string current) {
        if (!string.IsNullOrEmpty(input)) {
            return input;
        }
        return current;
    }

    这将具有与您试图寻找的相同的语义。

    这是不可能的,因为属性实际上是一对伪装的方法。每个属性都提供可用的getter和setter,这些getter和setter可以通过类似字段的语法进行访问。当您试图按照您的建议调用GetString时,您传递的是一个值而不是一个变量。您要传递的值是从getter get_WorkPhone返回的值。


    您可以尝试创建一个对象来保存属性值。这样,您就可以传递对象,并且仍然可以访问其中的属性。


    您不能访问ref属性,但如果您的函数同时需要getset访问,则可以传递定义了属性的类的实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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;
        }
    }

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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);
        }
    }