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

Passing properties by reference in C#

提问人:yogibear 提问时间:9/10/2009 最后编辑:AFractyogibear 更新时间:5/14/2023 访问量:180510

问:

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

GetString(
    inputString,
    ref Client.WorkPhone)

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

这给了我一个编译错误。我认为我想要实现的目标非常清楚。基本上,我想将输入字符串的内容复制到 的属性中。GetStringWorkPhoneClient

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

C# 属性 按引用传递

评论

1赞 nawfal 2/11/2013
至于为什么,请看这个 stackoverflow.com/questions/564557/......
1赞 Red Riding Hood 7/21/2021
我建议人们也看看这篇文章,了解涉及扩展方法的想法:stackoverflow.com/a/9601914/4503491

答:

3赞 jason 9/10/2009 #1

这是不可能的。你可以说

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

其中 是可写属性,并且 的定义更改为WorkPhonestringGetString

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

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

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

4赞 JaredPar 9/10/2009 #2

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

523赞 Nathan Baulch 9/10/2009 #3

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

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

评论

3赞 BlackjacketMack 9/5/2012
喜欢这些例子。我发现这也是扩展方法的好地方: public static string GetValueOrDefault(this string s, string isNullString) { if (s == null) { s = isNullString; } return s; } void Main(){ person.MobilePhone.GetValueOrDefault(person.工作电话);}code
11赞 Jaider 11/6/2012
在解决方案 2 中,第二个参数是不必要的。getOutput
38赞 Jaider 11/6/2012
我认为解决方案 3 的更好名称是 Reflection。
5赞 iCollect.it Ltd 6/6/2013
使用反射 AND Linq 表达式的解决方案 3 非常优雅,并且很好地完成了这项工作。4 年后,仍然做得很好:)
9赞 Tim Schmelter 2/11/2016
@GoneCodingGoodbye:但效率最低。使用反射来简单地为属性赋值就像拿大锤敲坚果一样。此外,应该设置属性的方法显然被错误地命名了。GetString
1赞 Anthony Reese 6/2/2010 #4

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

34赞 Firo 2/2/2012 #5

不复制属性

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赞 Camilo Martin 2/11/2013
+1 将名称更改为 ,因为前者在这里没有意义。GetStringNullSafeSet
3赞 supercat 12/17/2012 #6

另一个尚未提到的技巧是拥有实现属性的类(例如 )还定义一个委托并实现一个方法(可能还有两个和三个“额外参数”的版本),该方法会将其内部表示形式作为参数传递给所提供的过程。与其他处理该物业的方法相比,这有几个很大的优势:FooBardelegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)Fooref

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

传递事物是一种极好的模式;可惜它没有被更多地使用。ref

4赞 Zick Zhang 9/2/2015 #7

只是对 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);
        }
    }
}
40赞 Sven 4/19/2017 #8

我使用 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赞 mancze 9/15/2017
最好的答案在这里。您知道对性能有什么影响吗?如果能在答案中涵盖它就好了。我对表达式树不太熟悉,但我希望,使用 Compile() 意味着访问器实例实际上包含 IL 编译的代码,因此使用恒定数量的访问器 n 次是可以的,但使用总共 n 个访问器(高 ctor 成本)不会。
0赞 Eric Ouellet 5/6/2020
很棒的代码!我认为,这是最好的答案。最通用的。就像 mancze 说的......它应该对性能产生巨大影响,并且应该只在代码清晰度比性能更重要的上下文中使用。
2赞 bornfromanegg 10/26/2020
@EricOuellet“它应该对性能产生巨大影响”。基于什么?假设不是每次都重新创建类,我希望对 Get() 和 Set() 的调用对性能的影响最小。当然,正确的答案是测量它并找出答案。Accessor<T>
0赞 Eric Ouellet 6/3/2021
很棒的代码!!!我喜欢。只想说性能我错了,只是意识到它是编译的。我现在正在重复使用它,并且应该更多地重复使用它。👍
0赞 stoj 9/18/2022
这是通过基于编译器从通过 ctor 传入的 lambda 表达式发出的表达式为 setter/getter 创建自定义表达式的启发性思维。仅供参考,我更进一步,删除了不必要的反射、自定义 getter 表达式和特定字段表达式。stackoverflow.com/a/73762917/227110
0赞 chess123mate 10/9/2017 #9

你不能使用属性,但如果你的函数需要两者和访问权限,你可以传递一个定义了属性的类的实例:refgetset

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);
    }
}
13赞 Pellet 2/22/2018 #10

如果要同时获取和设置属性,可以在 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);
    }
}
2赞 macedo123 6/29/2019 #11

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

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
    }
}
1赞 palota 10/9/2019 #12

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

var phone = Client.WorkPhone;
GetString(input, ref phone);
Client.WorkPhone = phone;
1赞 Nicholas Petersen 3/11/2020 #13

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

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

0赞 warren.sentient 12/21/2021 #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 类中,也可以放在任何有意义的位置。

-1赞 knile 8/7/2022 #15

是的,你不能传递一个属性,但你可以将你的属性转换为一个带有支持字段的属性,并执行类似的事情。

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

但是有更好的解决方案,例如操作委托等。

4赞 stoj 9/18/2022 #16

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