Melewati properti dengan referensi dalam C #

224

Saya coba lakukan hal berikut:

GetString(
    inputString,
    ref Client.WorkPhone)

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

Ini memberi saya kesalahan kompilasi. Saya pikir cukup jelas apa yang saya coba capai. Pada dasarnya saya ingin GetStringmenyalin isi string input ke WorkPhoneproperti Client.

Apakah mungkin untuk melewati properti dengan referensi?

yogibear
sumber
Mengenai alasannya, lihat ini stackoverflow.com/questions/564557/…
nawfal

Jawaban:

423

Properti tidak dapat dilewati dengan referensi. Berikut adalah beberapa cara untuk mengatasi keterbatasan ini.

1. Nilai Pengembalian

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

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. Ekspresi 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. Refleksi

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
sumber
2
Sukai contohnya. Saya menemukan bahwa ini adalah tempat yang bagus untuk metode ekstensi juga: codestring public static GetValueOrDefault (string ini s, string isNullString) {if (s == null) {s = isNullString; } return s; } membatalkan Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack
9
Dalam solusi 2, parameter ke-2 getOutputtidak perlu.
Jaider
31
Dan saya pikir nama yang lebih baik untuk solusi 3 adalah Refleksi.
Jaider
1
Dalam solusi 2, parameter 2 getOutput tidak perlu - benar tetapi saya menggunakannya di dalam GetString untuk melihat nilai apa yang saya setting. Tidak yakin bagaimana melakukannya tanpa parameter ini.
Petras
3
@GoneCodingGoodbye: tetapi pendekatan yang paling tidak efisien. Menggunakan refleksi untuk memberikan nilai pada properti sama seperti mengambil palu untuk memecahkan kacang. Juga, metode GetStringyang seharusnya mengatur properti jelas salah nama.
Tim Schmelter
27

tanpa menduplikasi properti

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
sumber
4
+1 untuk mengubah nama GetStringmenjadi NullSafeSet, karena yang pertama tidak masuk akal di sini.
Camilo Martin
25

Saya menulis pembungkus menggunakan varian ExpressionTree dan c # 7 (jika seseorang tertarik):

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

Dan gunakan seperti:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");
Sven
sumber
3
Jawaban terbaik di sini. Apakah Anda tahu apa dampak kinerja? Akan lebih baik untuk dibahas dalam jawaban. Saya tidak terlalu familiar dengan pohon ekspresi tetapi saya berharap, bahwa menggunakan Compile () berarti instance accessor sebenarnya mengandung kode yang dikompilasi IL dan oleh karena itu menggunakan jumlah konstan accessor n-kali akan baik-baik saja, tetapi menggunakan total n accessor ( biaya ctor tinggi) tidak mau.
mancze
Kode hebat! Pendapat saya, itu adalah jawaban terbaik. Yang paling umum. Seperti kata mancze ... Ini harus memiliki dampak besar pada kinerja dan harus digunakan hanya dalam konteks di mana kejelasan kode lebih penting daripada kinerja.
Eric Ouellet
5

Jika Anda ingin mendapatkan dan mengatur properti keduanya, Anda dapat menggunakan ini di 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);
    }
}
Pelet
sumber
3

Trik lain yang belum disebutkan adalah memiliki kelas yang mengimplementasikan properti (misalnya Footipe Bar) juga mendefinisikan delegasi delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);dan mengimplementasikan metode ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(dan mungkin versi untuk dua dan tiga "parameter tambahan" juga) yang akan melewati representasi internal Foountuk prosedur yang disediakan sebagai refparameter. Ini memiliki beberapa keuntungan besar dibandingkan metode lain untuk bekerja dengan properti:

  1. Properti diperbarui "di tempat"; jika properti adalah tipe yang kompatibel dengan metode `Interlocked`, atau jika itu adalah struct dengan bidang terbuka dari tipe seperti itu, metode` Interlocked` dapat digunakan untuk melakukan pembaruan atom ke properti.
  2. Jika properti adalah struktur bidang terbuka, bidang struktur dapat dimodifikasi tanpa harus membuat salinan yang berlebihan.
  3. Jika metode `ActByRef` melewati satu atau lebih parameter` ref` dari pemanggilnya ke delegasi yang disediakan, dimungkinkan untuk menggunakan delegasi tunggal atau statis, sehingga menghindari kebutuhan untuk membuat penutupan atau delegasi pada saat run-time.
  4. Properti tahu kapan "dikerjakan". Meskipun selalu diperlukan untuk menggunakan hati-hati mengeksekusi kode eksternal sambil memegang kunci, jika seseorang dapat mempercayai penelepon untuk tidak melakukan apa pun dalam panggilan balik mereka yang mungkin memerlukan kunci lain, mungkin praktis untuk memiliki metode menjaga akses properti dengan mengunci, sedemikian sehingga pembaruan yang tidak kompatibel dengan `CompareExchange` masih dapat dilakukan secara atom.

Melewati segala sesuatu refadalah pola yang sangat baik; Sayang sekali itu tidak digunakan lagi.

supercat
sumber
3

Hanya sedikit ekspansi ke solusi Ekspresi Linq Nathan . Gunakan param multi generik sehingga properti tidak terbatas pada string.

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
sumber
2

Ini tercakup dalam bagian 7.4.1 dari spesifikasi bahasa C #. Hanya referensi-variabel yang dapat diberikan sebagai parameter ref atau out dalam daftar argumen. Properti tidak memenuhi syarat sebagai referensi variabel dan karenanya tidak dapat digunakan.

JaredPar
sumber
2

Ini tidak mungkin. Anda bisa mengatakan

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

di mana properti yang WorkPhonedapat ditulisi stringdan definisi GetStringdiubah menjadi

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

Ini akan memiliki semantik yang sama seperti yang Anda coba.

Ini tidak mungkin karena properti benar-benar sepasang metode yang menyamar. Setiap properti menyediakan getter dan setter yang dapat diakses melalui sintaksis mirip bidang. Ketika Anda mencoba menelepon GetStringseperti yang Anda usulkan, apa yang Anda sampaikan adalah nilai dan bukan variabel. Nilai yang Anda lewati adalah yang dikembalikan dari pengambil get_WorkPhone.

jason
sumber
1

Apa yang bisa Anda coba lakukan adalah membuat objek untuk menyimpan nilai properti. Dengan begitu Anda bisa melewati objek dan masih memiliki akses ke properti di dalamnya.

Anthony Reese
sumber
1

Properti tidak bisa diteruskan dengan referensi? Jadikan sebagai bidang, lalu gunakan properti untuk merujuknya secara publik:

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
sumber
0

Anda tidak dapat refmemiliki properti, tetapi jika fungsi Anda membutuhkan keduanya getdan setakses, Anda dapat membagikan turunan kelas dengan properti yang ditentukan:

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

Contoh:

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);
    }
}
teman catur123
sumber
0

Jawaban yang diterima baik jika fungsi itu ada dalam kode Anda dan Anda dapat memodifikasinya. Tetapi kadang-kadang Anda harus menggunakan objek dan fungsi dari beberapa perpustakaan eksternal dan Anda tidak dapat mengubah definisi properti dan fungsi. Kemudian Anda bisa menggunakan variabel sementara.

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

Untuk memberi suara pada masalah ini, berikut adalah satu saran aktif tentang bagaimana ini dapat ditambahkan ke bahasa. Saya tidak mengatakan ini adalah cara terbaik untuk melakukan ini (sama sekali), jangan ragu untuk mengeluarkan saran Anda sendiri. Tetapi membiarkan properti untuk diteruskan oleh ref seperti Visual Basic sudah dapat lakukan akan sangat membantu menyederhanakan beberapa kode, dan cukup sering!

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

Nicholas Petersen
sumber