Ü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?

Yogi Bär
quelle
Informationen
nawfal

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");
}
Nathan Baulch
quelle
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);
    }
}
Firo
quelle
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");
Sven
quelle
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);
    }
}
Pellet
quelle
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.

Superkatze
quelle
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);
        }
    }
}
Zick Zhang
quelle
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.

JaredPar
quelle
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.

Jason
quelle
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.

Anthony Reese
quelle
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
    }
}
macedo123
quelle
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);
    }
}
chess123mate
quelle
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;
Palota
quelle
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

Nicholas Petersen
quelle