【问题标题】:Passing properties by reference in C#在 C# 中通过引用传递属性
【发布时间】:2010-11-27 00:45:03
【问题描述】:

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

GetString(
    inputString,
    ref Client.WorkPhone)

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

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

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

【问题讨论】:

标签: c# properties pass-by-reference


【解决方案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))
    {
        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");
}

【讨论】:

  • 喜欢这些例子。我发现这也是扩展方法的好地方:codepublic static string GetValueOrDefault(this string s, string isNullString) { if (s == null) { s = isNullString; } 返回 s; } void Main(){ person.MobilePhone.GetValueOrDefault(person.WorkPhone); }
  • 在方案2中,第二个参数getOutput是不必要的。
  • 我认为解决方案 3 的更好名称是反射。
  • 使用 reflectionLinq 表达式 的解决方案 3 非常优雅并且做得很好。 4 年后,仍然做得很好:)
  • @GoneCodingGoodbye:但效率最低的方法。使用反射简单地为属性赋值就像用大锤敲击坚果一样。此外,应该设置属性的方法 GetString 显然命名错误。
【解决方案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);
    }
}

【讨论】:

  • +1 用于将名称 GetString 更改为 NullSafeSet,因为前者在这里没有意义。
【解决方案3】:

我使用 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");

【讨论】:

  • 最佳答案在这里。你知道什么是性能影响吗?将它包含在答案中会很好。我不太熟悉表达式树,但我希望,使用 Compile() 意味着访问器实例包含实际的 IL 编译代码,因此使用恒定数量的访问器 n 次是可以的,但使用总共 n 个访问器(高 ctor 成本)不会。
  • 很棒的代码!我的意见,这是最好的答案。最通用的一种。就像 mancze 所说的那样......它应该对性能产生巨大影响,并且应该只在代码清晰比性能更重要的情况下使用。
  • @EricOuellet “它应该会对性能产生巨大影响”。基于什么?假设 Accessor&lt;T&gt; 类不是每次都重新创建,我希望对 Get() 和 Set() 的调用对性能的影响最小。当然,正确的答案是测量它并找出答案。
  • 很棒的代码!!!!我喜欢它。只想说性能,我错了,才意识到它是编译的。我现在正在重用它,应该更多地重用它。 ?
【解决方案4】:

如果你想同时获取和设置属性,你可以在 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);
    }
}

【讨论】:

    【解决方案5】:

    只是对Nathan's Linq Expression solution 的一点扩展。使用多泛型参数,使属性不限于字符串。

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

    【讨论】:

      【解决方案6】:

      C# 语言规范的第 7.4.1 节对此进行了介绍。只有变量引用可以作为参数列表中的 ref 或 out 参数传递。属性不符合变量引用的条件,因此不能使用。

      【讨论】:

        【解决方案7】:

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

        1. 属性已“就地”更新;如果属性的类型与 `Interlocked` 方法兼容,或者如果它是具有此类类型的公开字段的结构,则可以使用 `Interlocked` 方法对属性执行原子更新。
        2. 如果属性是公开字段结构,则可以修改结构的字段,而无需对其进行任何冗余副本。
        3. 如果 `ActByRef` 方法将一个或多个 `ref` 参数从其调用者传递给提供的委托,则可以使用单例或静态委托,从而避免在运行时创建闭包或委托 -时间。
        4. 该属性知道何时“使用”它。虽然在持有锁的同时执行外部代码时总是需要谨慎,但如果可以相信调用者不会在其回调中做任何可能需要另一个锁的事情,那么让该方法使用锁,这样与 `CompareExchange` 不兼容的更新仍然可以准原子地执行。

        传递东西是ref 是一个很好的模式;太糟糕了,它没有更多地使用。

        【讨论】:

          【解决方案8】:

          这是不可能的。你可以说

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

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

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

          这将具有您似乎正在尝试的相同语义。

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

          【讨论】:

            【解决方案9】:

            属性不能通过引用传递?然后将其设为字段,并使用该属性公开引用它:

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

            【讨论】:

              【解决方案10】:

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

              【讨论】:

                【解决方案11】:

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

                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】:

                  如果该函数在您的代码中并且您可以修改它,则接受的答案是好的。但有时您必须使用某个外部库中的对象和函数,并且无法更改属性和函数定义。然后你可以只使用一个临时变量。

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

                  【讨论】:

                    【解决方案13】:

                    要对此问题进行投票,这里有一个关于如何将其添加到语言中的积极建议。我并不是说这是最好的方法(完全),请随时提出您自己的建议。但是允许像 Visual Basic 那样通过 ref 传递属性将极大地帮助简化一些代码,而且经常如此!

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

                    【讨论】:

                      【解决方案14】:

                      您似乎需要对该字段施加业务规则约束,同时希望尽可能保持代码 DRY。

                      这是可以实现的,并且还可以通过在该字段上实现完整属性并使用您的可重用方法来保留您的域语义:

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

                      SafeSetString 方法可以放在 Utilities 类中或任何有意义的地方。

                      【讨论】:

                        猜你喜欢
                        • 2011-01-23
                        • 2010-12-24
                        • 2014-11-08
                        • 1970-01-01
                        • 1970-01-01
                        • 2018-07-09
                        • 1970-01-01
                        相关资源
                        最近更新 更多