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

297

我正在尝试做以下事情:

GetString(
    inputString,
    ref Client.WorkPhone)

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

这段代码给我编译错误。我认为我的意图很清楚。基本上,我想让 GetString 将输入字符串的内容复制到 ClientWorkPhone 属性中。

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


1
关于为什么,请参见此链接:https://dev59.com/CnRB5IYBdhLWcg3wpYmo - nawfal
1
我建议大家也可以查看这篇帖子,了解有关扩展方法的想法:https://dev59.com/92kw5IYBdhLWcg3w2-PG#9601914 - Red Riding Hood
16个回答

523

属性不能通过引用传递。以下是几种可以解决此限制的方法。

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

3
喜欢这些示例。我发现这也是扩展方法的一个很好的应用场景:codepublic static string GetValueOrDefault(this string s, string isNullString) { if (s == null) { s = isNullString; } return s; } void Main(){ person.MobilePhone.GetValueOrDefault(person.WorkPhone); } - BlackjacketMack
11
解决方案2中,第二个参数 getOutput 是不必要的。 - Jaider
38
我认为解决方案3更好的名称应该是"反思"。 - Jaider
5
解决方案3使用反射Linq表达式非常优雅,并且很好地完成了工作。四年过去了,仍然做得很好 :) - iCollect.it Ltd
9
使用反射来简单地为属性分配一个值是效率最低的方法。这就像用大锤去砸蚂蚁一样。此外,一个名为GetString的方法却被用来设置属性,显然命名不当。 - Tim Schmelter
显示剩余9条评论

40
我使用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个访问器(构造函数代价高)就不行了。 - mancze
很棒的代码!在我看来,这是最好的答案。最通用的一个。就像mancze所说...它应该对性能有巨大的影响,并且只应在代码清晰度比性能更重要的情况下使用。 - Eric Ouellet
2
“它应该对性能产生巨大影响”。根据什么?假设 Accessor<T> 类不会每次重新创建,我预计调用 Get() 和 Set() 的影响对性能应该很小。当然,正确的答案是测量并找出确切的影响。 - bornfromanegg
太棒了的代码!!!我喜欢它。只是想说一下关于性能的事情,我错了,现在才意识到它已经编译过了。我现在正在重复使用它,应该更多地重复使用它。 - Eric Ouellet
这是一种启发式的思考方式,通过为基于通过ctor传递的lambda表达式发出的编译器表达式创建自定义表达式来实现setter/getter。值得注意的是,我进一步去除了不必要的反射、自定义getter表达式和专门的字段表达式。https://dev59.com/lnM_5IYBdhLWcg3wXyDZ#73762917 - stoj
看到访问器语法,它非常让我想起了ReactiveUI的WhenAny(),如果传递引用的目标是为了能够观察值的变化,那么使用IObservable<T>可能是更好的选择。 - Alexander Gräf

34

不重复地使用该属性

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

6
将名称从“GetString”更改为“NullSafeSet”的建议获得+1,因为前者在这里没有意义。 - Camilo Martin

13

如果你想要同时获取和设置属性,你可以在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.get()))
    {
        outValue.set(inValue);
    }
}

4
Sven的表达式树解决方案启发,该解决方案将属性包装在通用的“访问器”类中,作为c#缺乏本地“通过引用传递属性”的支持的解决方法。下面是一个简化版本,它:
  • 不依赖反射
  • 删除不必要的自定义getter实例
  • 处理属性和字段相同
    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 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;
    }

用法..

using System;
using NUnit.Framework;

namespace Utils.Tests;

public class AccessorTest
{
    private class TestClass
    {
        public string Property { get; set; }
    }

    [Test]
    public void Test()
    {
        var testClass = new TestClass { Property = "a" };

        var accessor = new Accessor<string>(() => testClass.Property);
        Assert.That(accessor.Get(), Is.EqualTo("a"));

        accessor.Set("b");
        Assert.That(testClass.Property, Is.EqualTo("b"));
        Assert.That(accessor.Get(), Is.EqualTo("b"));
    }
}

4
这在C#语言规范的7.4.1节中有详细说明。只有变量引用可以作为参数列表中的ref或out参数传递。属性不符合变量引用的条件,因此无法使用。

4

Nathan的Linq表达式解决方案做一个小的扩展。使用多个泛型参数,使属性不仅局限于字符串。

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

3
这是不可能的。你可以说:
Client.WorkPhone = GetString(inputString, Client.WorkPhone);

其中WorkPhone是一个可写的字符串属性,而GetString的定义已经改变为:

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

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

3
另一个尚未提及的技巧是让实现属性的类(例如类型为BarFoo)定义一个委托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传递参数是一种很好的模式,可惜它没有被更广泛地使用。


2

属性无法通过引用传递?那就将其变为字段,并使用属性在公共范围内引用它:

最初的回答

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接