ผ่านคุณสมบัติโดยการอ้างอิงใน C #


224

ฉันกำลังพยายามทำสิ่งต่อไปนี้:

GetString(
    inputString,
    ref Client.WorkPhone)

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

นี่ทำให้ฉันมีข้อผิดพลาดในการคอมไพล์ ฉันคิดว่ามันชัดเจนว่าฉันพยายามทำอะไร โดยทั่วไปฉันต้องการGetStringคัดลอกเนื้อหาของสตริงการป้อนข้อมูลให้กับทรัพย์สินของWorkPhoneClient

เป็นไปได้หรือไม่ที่จะส่งทรัพย์สินโดยอ้างอิง?


สำหรับสาเหตุที่ดูstackoverflow.com/questions/564557/
nawfal

คำตอบ:


423

ไม่สามารถส่งผ่านคุณสมบัติโดยอ้างอิง ต่อไปนี้เป็นวิธีที่คุณสามารถแก้ไขข้อ จำกัด นี้ได้

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

2
รักตัวอย่าง ฉันพบว่านี่เป็นสถานที่ที่ยอดเยี่ยมสำหรับวิธีการขยายเช่นกัน: codeสตริงคงที่สาธารณะ GetValueOrDefault (สตริงนี้ s, สตริง isNullString) {ถ้า (s == null) {s = isNullString; } คืน s; } ถือเป็นโมฆะหลัก () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack

9
ในโซลูชัน 2 พารามิเตอร์ที่ 2 getOutputไม่จำเป็น
Jaider

31
และฉันคิดว่าชื่อที่ดีกว่าสำหรับวิธีที่ 3 คือ Reflection
Jaider

1
ในโซลูชัน 2 พารามิเตอร์ที่ 2 getOutput ไม่จำเป็น - จริง แต่ฉันใช้ภายใน GetString เพื่อดูว่าค่าที่ฉันตั้งไว้คืออะไร ไม่แน่ใจว่าจะทำอย่างไรหากไม่มีพารามิเตอร์นี้
Petras

3
@GoneCodingGoodbye: แต่วิธีการที่มีประสิทธิภาพน้อยที่สุด การใช้การไตร่ตรองเพื่อกำหนดค่าให้กับทรัพย์สินก็เหมือนกับการเอาค้อนขนาดใหญ่มาหักแตก นอกจากนี้วิธีการGetStringที่ควรตั้งค่าคุณสมบัตินั้นผิดอย่างชัดเจน
Tim Schmelter

27

โดยไม่ต้องทำซ้ำทรัพย์สิน

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 สำหรับการเปลี่ยนชื่อGetStringเป็นNullSafeSetเพราะอดีตทำให้ไม่เหมาะสมที่นี่
Camilo Martin

25

ฉันเขียน wrapper โดยใช้ตัวแปร 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");

3
คำตอบที่ดีที่สุดที่นี่ คุณรู้หรือไม่ว่าอะไรคือประสิทธิภาพที่ได้รับผลกระทบ? มันจะดีถ้ามีคำตอบอยู่ในนั้น ฉันไม่คุ้นเคยกับต้นไม้นิพจน์มากนัก แต่ฉันคาดหวังว่าการใช้ Compile () หมายถึงอินสแตนซ์ของผู้เข้าถึงนั้นมีรหัสที่รวบรวม IL จริง ๆ ดังนั้นการใช้ accessors จำนวนคงที่ n-times ก็ถือว่าโอเค แต่การใช้ทั้งหมด ต้นทุน ctor สูง) จะไม่
mancze

รหัสที่ดี! ความคิดเห็นของฉันมันเป็นคำตอบที่ดีที่สุด หนึ่งที่พบมากที่สุด อย่างที่บอกว่า mancze ... มันควรจะมีผลกระทบอย่างมากต่อประสิทธิภาพการทำงานและควรใช้ในบริบทที่ความชัดเจนของรหัสสำคัญกว่า perfomance
Eric Ouellet

5

หากคุณต้องการได้รับและตั้งค่าคุณสมบัติทั้งสองคุณสามารถใช้สิ่งนี้ใน 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);
    }
}

3

เคล็ดลับก็ยังไม่ได้กล่าวถึงคือการมีชั้นเรียนซึ่งดำเนินอสังหาริมทรัพย์ (เช่น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พารามิเตอร์ นี่เป็นข้อได้เปรียบสองสามข้อที่ใหญ่กว่าวิธีอื่น ๆ ในการใช้งานคุณสมบัติ:

  1. สถานที่ให้บริการมีการปรับปรุง "ในสถานที่"; ถ้าคุณสมบัติเป็นชนิดที่เข้ากันได้กับวิธีการ 'Interlocked' หรือถ้ามันเป็นโครงสร้างที่มีเขตข้อมูลที่เปิดเผยประเภทดังกล่าววิธีการ 'Interlocked' อาจถูกใช้เพื่อดำเนินการปรับปรุงอะตอมของคุณสมบัติ
  2. หากคุณสมบัติเป็นโครงสร้างฟิลด์เปิดเผยฟิลด์ของโครงสร้างอาจถูกปรับเปลี่ยนโดยไม่ต้องทำสำเนาซ้ำซ้อนใด ๆ
  3. หากวิธี `ActByRef` ส่งพารามิเตอร์` ref` อย่างน้อยหนึ่งพารามิเตอร์จากผู้เรียกไปยังผู้รับมอบสิทธิ์ที่ระบุไว้อาจเป็นไปได้ที่จะใช้ตัวแทนผู้ติดต่อแบบเดี่ยวหรือแบบคงที่ดังนั้นหลีกเลี่ยงความจำเป็นในการสร้างการปิดหรือผู้รับมอบสิทธิ์ในเวลาทำงาน
  4. สถานที่ให้บริการรู้ว่าเมื่อมันถูก "ทำงานกับ" ในขณะที่จำเป็นต้องใช้ความระมัดระวังในการเรียกใช้โค้ดภายนอกในขณะที่ถือล็อคอยู่หากผู้ใช้สามารถไว้วางใจผู้โทรไม่ต้องทำสิ่งใดในการโทรกลับที่อาจต้องใช้การล็อกอีกครั้ง ล็อคซึ่งการอัปเดตที่ไม่สามารถใช้งานร่วมกับ

การผ่านสิ่งต่าง ๆrefเป็นรูปแบบที่ยอดเยี่ยม แย่มากที่มันไม่ได้ใช้งานมากขึ้น


3

เพียงแค่การขยายตัวเพียงเล็กน้อยที่จะแก้ปัญหาการแสดงออกของนาธาน Linq ใช้หลายพารามิเตอร์ทั่วไปเพื่อให้คุณสมบัติไม่ จำกัด เฉพาะสตริง

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

สิ่งนี้จะกล่าวถึงในหัวข้อ 7.4.1 ของข้อกำหนดภาษา C # การอ้างอิงตัวแปรสามารถส่งผ่านเป็นพารามิเตอร์ ref หรือ out ในรายการอาร์กิวเมนต์ สถานที่ให้บริการไม่ได้มีคุณสมบัติเป็นการอ้างอิงตัวแปรและจึงไม่สามารถใช้


2

สิ่งนี้เป็นไปไม่ได้ คุณสามารถพูดได้

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

โดยที่WorkPhoneเป็นstringคุณสมบัติที่เขียนได้และคำจำกัดความของGetStringเปลี่ยนเป็น

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

สิ่งนี้จะมีความหมายเดียวกันกับที่คุณพยายามทำ

สิ่งนี้เป็นไปไม่ได้เนื่องจากสถานที่ให้บริการเป็นวิธีการปลอมตัวคู่หนึ่ง แต่ละคุณสมบัติทำให้ getters และ setters พร้อมใช้งานที่สามารถเข้าถึงได้ผ่านทางไวยากรณ์ที่เหมือนฟิลด์ เมื่อคุณพยายามโทรGetStringตามที่คุณเสนอสิ่งที่คุณกำลังส่งผ่านคือค่าไม่ใช่ตัวแปร ค่าที่คุณจะผ่านในคือการที่กลับมาจาก get_WorkPhonegetter


1

สิ่งที่คุณสามารถลองทำได้คือสร้างวัตถุเพื่อเก็บค่าคุณสมบัติ ด้วยวิธีนี้คุณสามารถผ่านวัตถุและยังสามารถเข้าถึงคุณสมบัติภายใน


1

ไม่สามารถส่งผ่านคุณสมบัติโดยการอ้างอิงได้ กำหนดให้เป็นฟิลด์จากนั้นใช้คุณสมบัติเพื่อการอ้างอิงต่อสาธารณะ:

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

คุณไม่สามารถ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);
    }
}

0

คำตอบที่ยอมรับนั้นดีถ้าฟังก์ชันนั้นอยู่ในรหัสของคุณและคุณสามารถแก้ไขได้ แต่บางครั้งคุณต้องใช้วัตถุและฟังก์ชั่นจากห้องสมุดภายนอกบางแห่งและคุณไม่สามารถเปลี่ยนคุณสมบัติและนิยามของฟังก์ชันได้ จากนั้นคุณสามารถใช้ตัวแปรชั่วคราว

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

0

หากต้องการลงคะแนนให้กับปัญหานี้ต่อไปนี้เป็นคำแนะนำที่ใช้งานได้อย่างหนึ่งเกี่ยวกับวิธีการเพิ่มลงในภาษานี้ ฉันไม่ได้พูดว่านี่เป็นวิธีที่ดีที่สุดในการทำเช่นนี้ (อย่าลังเลที่จะแนะนำข้อเสนอแนะของคุณเอง แต่การอนุญาตให้คุณสมบัติถูกส่งผ่านโดยอ้างอิงเช่น Visual Basic แล้วสามารถทำได้อย่างมหาศาลจะช่วยลดความซับซ้อนของรหัสบางอย่างและค่อนข้างบ่อย!

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.