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

我想做的是:

GetString(
inputString,
ref Client.WorkPhone)


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

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

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

167274 次浏览

这是不可能的。你可以说

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返回的。

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

属性不能通过引用传递。这里有一些方法可以绕过这个限制。

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

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

没有复制属性

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

另一个尚未提及的技巧是让实现属性的类(例如Bar类型的Foo)也定义一个委托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. 将属性更新为“in place”;如果属性的类型与' Interlocked '方法兼容,或者它是一个具有此类类型的公开字段的结构体,则' Interlocked '方法可以用于对属性执行原子更新。
  2. 如果属性是一个暴露字段结构,结构的字段可以被修改,而不必对其进行任何冗余拷贝。 如果' ActByRef '方法将一个或多个' ref '参数从调用者传递给所提供的委托,则可以使用单例或静态委托,从而避免在运行时创建闭包或委托。
  3. 属性知道它什么时候被“使用”。虽然在持有锁时执行外部代码总是需要谨慎,但如果可以信任调用者不会在回调中做任何可能需要另一个锁的事情,那么让方法用锁保护属性访问可能是可行的,这样与' CompareExchange '不兼容的更新仍然可以准原子地执行。

传递对象ref是一个很好的模式;可惜它没有被更多地使用。

只是对内森的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);
}
}
}

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

你不能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);
}
}

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

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

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

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

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

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

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

看起来,您需要对该字段施加业务规则约束,同时又希望使您的代码尽可能地保持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类中或任何有意义的地方。

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

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

但有更好的解决方案,比如动作委托等。

斯文的表达式树解的启发,下面是一个不依赖反射的简化版本。此外,它还删除了不必要的自定义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 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;
}