簡體   English   中英

在 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將輸入字符串的內容復制到ClientWorkPhone屬性。

是否可以通過引用傳遞屬性?

屬性不能通過引用傳遞。 您可以通過以下幾種方法來解決此限制。

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

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

如果你想同時獲取和設置屬性,你可以在 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);
    }
}

只是對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);
        }
    }
}

另一個尚未提及的技巧是讓實現屬性的類(例如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. 該屬性已“就地”更新; 如果屬性的類型與 `Interlocked` 方法兼容,或者它是具有此類類型公開字段的結構,則可以使用 `Interlocked` 方法對屬性執行原子更新。
  2. 如果該屬性是一個公開字段結構,則可以修改該結構的字段,而無需對其進行任何冗余副本。
  3. 如果`ActByRef` 方法將一個或多個`ref` 參數從其調用者傳遞給提供的委托,則可能使用單例或靜態委托,從而避免在運行時創建閉包或委托的需要。
  4. 該屬性知道它何時被“使用”。 雖然在持有鎖時執行外部代碼總是需要謹慎,但如果可以相信調用者不會在他們的回調中做任何可能需要另一個鎖的事情,那么讓該方法使用鎖定,這樣與 `CompareExchange` 不兼容的更新仍然可以准原子地執行。

將事物傳遞給ref是一種極好的模式; 太糟糕了,它沒有被更多地使用。

這在 C# 語言規范的第 7.4.1 節中有介紹。 只有變量引用可以作為參數列表中的 ref 或 out 參數傳遞。 屬性不符合變量引用的條件,因此不能使用。

這不可能。 你可以說

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

屬性不能通過引用傳遞? 然后將其設為字段,並使用該屬性公開引用它:

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

您可以嘗試做的是創建一個對象來保存屬性值。 這樣你就可以傳遞對象並且仍然可以訪問里面的屬性。

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

如果該功能在您的代碼中並且您可以修改它,那么接受的答案是好的。 但有時您必須使用某個外部庫中的對象和函數,並且無法更改屬性和函數定義。 然后你可以只使用一個臨時變量。

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

為了對這個問題進行投票,這里有一個關於如何將其添加到語言中的積極建議。 我並不是說這是最好的方法(完全),請隨意提出您自己的建議。 但是允許屬性通過 ref 傳遞,就像 Visual Basic 已經可以做到的那樣,將極大地幫助簡化一些代碼,而且經常如此!

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

您似乎需要對該字段施加業務規則約束,同時希望盡可能保持代碼干燥。

通過在該字段上實現完整屬性並使用可重用的方法,它是可以實現的,並且還可以保留您的域語義:

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 的表達式樹解決方案的啟發,下面是一個不依賴反射的簡化版本。 此外,它還刪除了不必要的自定義 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;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM