提问人:yogibear 提问时间:9/10/2009 最后编辑:AFractyogibear 更新时间:5/14/2023 访问量:180510
在 C 中通过引用传递属性#
Passing properties by reference in C#
问:
我正在尝试执行以下操作:
GetString(
inputString,
ref Client.WorkPhone)
private void GetString(string inValue, ref string outValue)
{
if (!string.IsNullOrEmpty(inValue))
{
outValue = inValue;
}
}
这给了我一个编译错误。我认为我想要实现的目标非常清楚。基本上,我想将输入字符串的内容复制到 的属性中。GetString
WorkPhone
Client
是否可以通过引用传递属性?
答:
这是不可能的。你可以说
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。当您尝试按照您的建议进行调用时,您传入的是一个值,而不是一个变量。您传入的值是从 getter 返回的值。GetString
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");
}
评论
code
getOutput
GetString
您可以尝试创建一个对象来保存属性值。这样,您就可以传递对象,并且仍然可以访问内部的属性。
不复制属性
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);
}
}
评论
GetString
NullSafeSet
另一个尚未提到的技巧是拥有实现属性的类(例如 )还定义一个委托并实现一个方法(可能还有两个和三个“额外参数”的版本),该方法会将其内部表示形式作为参数传递给所提供的过程。与其他处理该物业的方法相比,这有几个很大的优势:Foo
Bar
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
- 该物业已“就地”更新;如果属性属于与“互锁”方法兼容的类型,或者如果它是具有此类公开字段的结构,则可以使用“互锁”方法对属性执行原子更新。
- 如果属性是公开字段结构,则可以修改结构的字段,而无需对其进行任何冗余副本。
- 如果“ActByRef”方法将一个或多个“ref”参数从其调用方传递到提供的委托,则可以使用单一实例或静态委托,从而避免在运行时创建闭包或委托的需要。
- 该物业知道何时被“处理”。虽然在持有锁时始终需要谨慎执行外部代码,但如果可以信任调用方不要在他们的回调中做任何可能需要另一个锁的事情,那么让该方法使用锁来保护属性访问可能是可行的,这样与“CompareExchange”不兼容的更新仍然可以准原子地执行。
传递事物是一种极好的模式;可惜它没有被更多地使用。ref
只是对 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);
}
}
}
我使用 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");
评论
Accessor<T>
你不能使用属性,但如果你的函数需要两者和访问权限,你可以传递一个定义了属性的类的实例:ref
get
set
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;
}
}
但是有更好的解决方案,例如操作委托等。
受 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"));
}
}
评论