Übergeben von Eigenschaften als Referenz in C #


224

Ich versuche Folgendes zu tun:

GetString(
    inputString,
    ref Client.WorkPhone)

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

Dies gibt mir einen Kompilierungsfehler. Ich denke, es ist ziemlich klar, was ich erreichen will. Grundsätzlich möchte ich GetStringden Inhalt einer Eingabezeichenfolge in die WorkPhoneEigenschaft von kopieren Client.

Ist es möglich, eine Eigenschaft als Referenz zu übergeben?


Antworten:


423

Eigenschaften können nicht als Referenz übergeben werden. Hier sind einige Möglichkeiten, wie Sie diese Einschränkung umgehen können.

1. Rückgabewert

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. Delegieren

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

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. Reflexion

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

2
Ich liebe die Beispiele. Ich finde, dass dies auch ein großartiger Ort für Erweiterungsmethoden ist: codeöffentliche statische Zeichenfolge GetValueOrDefault (diese Zeichenfolge s, Zeichenfolge isNullString) {if (s == null) {s = isNullString; } kehrt zurück; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack

9
In Lösung 2 ist der 2. Parameter nicht getOutputerforderlich.
Jaider

31
Und ich denke, ein besserer Name für die Lösung 3 ist Reflection.
Jaider

1
In Lösung 2 ist der zweite Parameter getOutput nicht erforderlich - true, aber ich habe ihn in GetString verwendet, um zu sehen, welchen Wert ich festgelegt habe. Ich bin mir nicht sicher, wie ich das ohne diesen Parameter machen soll.
Petras

3
@GoneCodingGoodbye: aber der am wenigsten effiziente Ansatz. Das Verwenden von Reflexion, um einer Eigenschaft einfach einen Wert zuzuweisen, ist wie das Nehmen eines Vorschlaghammers, um eine Nuss zu knacken. Außerdem ist eine Methode GetString, mit der eine Eigenschaft festgelegt werden soll, eindeutig falsch benannt.
Tim Schmelter

27

ohne die Eigenschaft zu duplizieren

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

4
+1 für die Namensänderung GetStringin NullSafeSet, da erstere hier keinen Sinn macht.
Camilo Martin

25

Ich habe einen Wrapper mit der ExpressionTree-Variante und c # 7 geschrieben (wenn jemand interessiert ist):

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

Und benutze es wie:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");

3
Beste Antwort hier. Wissen Sie, wie sich die Leistung auswirkt? Es wäre schön, wenn es in der Antwort behandelt würde. Ich bin mit den Ausdrucksbäumen nicht sehr vertraut, aber ich würde erwarten, dass die Verwendung von Compile () bedeutet, dass die Accessor-Instanz tatsächlich IL-kompilierten Code enthält. Daher wäre es in Ordnung, n-mal eine konstante Anzahl von Accessoren zu verwenden, aber insgesamt n Accessoren zu verwenden ( hohe ctor Kosten) würde nicht.
Mancze

Toller Code! Meiner Meinung nach ist es die beste Antwort. Die allgemeinste. Wie mancze sagt ... Es sollte einen großen Einfluss auf die Leistung haben und nur in einem Kontext verwendet werden, in dem die Klarheit des Codes wichtiger ist als die Leistung.
Eric Ouellet

5

Wenn Sie beide Eigenschaften abrufen und festlegen möchten, können Sie dies in C # 7 verwenden:

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

3

Ein weiterer Trick, der noch nicht erwähnt wurde, besteht darin, dass die Klasse, die eine Eigenschaft (z. B. Foovom Typ Bar) delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);implementiert, auch einen Delegaten definiert und eine Methode ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(und möglicherweise auch Versionen für zwei und drei "zusätzliche Parameter") implementiert, die ihre interne Darstellung von Fooan übergibt die mitgelieferte Prozedur als refParameter. Dies hat einige große Vorteile gegenüber anderen Methoden der Arbeit mit der Immobilie:

  1. Die Eigenschaft wird "an Ort und Stelle" aktualisiert; Wenn die Eigenschaft von einem Typ ist, der mit "Interlocked" -Methoden kompatibel ist, oder wenn es sich um eine Struktur mit exponierten Feldern solcher Typen handelt, können die "Interlocked" -Methoden verwendet werden, um atomare Aktualisierungen der Eigenschaft durchzuführen.
  2. Wenn es sich bei der Eigenschaft um eine exponierte Feldstruktur handelt, können die Felder der Struktur geändert werden, ohne dass redundante Kopien davon erstellt werden müssen.
  3. Wenn die ActByRef-Methode einen oder mehrere ref-Parameter von ihrem Aufrufer an den angegebenen Delegaten weiterleitet, kann möglicherweise ein einzelner oder statischer Delegat verwendet werden, sodass zur Laufzeit keine Closures oder Delegaten erstellt werden müssen.
  4. Die Immobilie weiß, wann mit ihr "gearbeitet" wird. Während es immer notwendig ist, beim Ausführen einer Sperre Vorsicht walten zu lassen, wenn man darauf vertrauen kann, dass Anrufer in ihrem Rückruf nichts tun, was möglicherweise eine andere Sperre erfordert, kann es praktisch sein, dass die Methode den Eigenschaftszugriff mit a schützt Sperren, sodass Aktualisierungen, die nicht mit "CompareExchange" kompatibel sind, weiterhin quasi atomar ausgeführt werden können.

Dinge weiterzugeben refist ein ausgezeichnetes Muster; Schade, dass es nicht mehr verwendet wird.


3

Nur eine kleine Erweiterung der Linq Expression-Lösung von Nathan . Verwenden Sie mehrere generische Parameter, damit die Eigenschaft nicht auf Zeichenfolgen beschränkt ist.

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

2

Dies wird in Abschnitt 7.4.1 der C # -Sprachenspezifikation behandelt. In einer Argumentliste kann nur eine Variablenreferenz als ref- oder out-Parameter übergeben werden. Eine Eigenschaft gilt nicht als Variablenreferenz und kann daher nicht verwendet werden.


2

Das ist nicht möglich. Du könntest sagen

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

Wo WorkPhoneist eine beschreibbare stringEigenschaft und die Definition von GetStringwird in geändert

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

Dies hat dieselbe Semantik, nach der Sie zu suchen scheinen.

Dies ist nicht möglich, da eine Eigenschaft wirklich ein verschleiertes Methodenpaar ist. Jede Eigenschaft stellt Getter und Setter zur Verfügung, auf die über eine feldähnliche Syntax zugegriffen werden kann. Wenn Sie versuchen, GetStringwie vorgeschlagen aufzurufen , übergeben Sie einen Wert und keine Variable. Der Wert, den Sie übergeben, ist der vom Getter zurückgegebene get_WorkPhone.


1

Sie können versuchen, ein Objekt zu erstellen, das den Eigenschaftswert enthält. Auf diese Weise können Sie das Objekt übergeben und haben dennoch Zugriff auf die darin befindliche Eigenschaft.


1

Eigenschaften können nicht als Referenz übergeben werden? Machen Sie es dann zu einem Feld und verwenden Sie die Eigenschaft, um es öffentlich zu referenzieren:

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

0

Sie können keine refEigenschaft festlegen, aber wenn Ihre Funktionen beides getund setZugriff benötigen , können Sie eine Instanz einer Klasse mit einer definierten Eigenschaft weitergeben:

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

Beispiel:

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

0

Die akzeptierte Antwort ist gut, wenn diese Funktion in Ihrem Code enthalten ist und Sie sie ändern können. Manchmal müssen Sie jedoch ein Objekt und eine Funktion aus einer externen Bibliothek verwenden, und Sie können die Eigenschafts- und Funktionsdefinition nicht ändern. Dann können Sie einfach eine temporäre Variable verwenden.

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

0

Um über dieses Thema abzustimmen, hier ein aktiver Vorschlag, wie dies der Sprache hinzugefügt werden könnte. Ich sage nicht, dass dies (überhaupt) der beste Weg ist, dies zu tun. Sie können gerne Ihren eigenen Vorschlag machen. Das Zulassen, dass Eigenschaften von ref übergeben werden, wie es Visual Basic bereits kann, würde jedoch erheblich dazu beitragen, Code zu vereinfachen, und das ziemlich oft!

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.