วิธีการโทรถ้าไม่ว่างใน C #


107

เป็นไปได้ไหมที่จะย่อข้อความนี้ให้สั้นลง?

if (obj != null)
    obj.SomeMethod();

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

และปัญหาที่คล้ายกันกับเหตุการณ์ที่

public event Func<string> MyEvent;

แล้วเรียกใช้

if (MyEvent != null)
    MyEvent.Invoke();

41
เราพิจารณาการเพิ่มตัวดำเนินการใหม่ใน C # 4: "obj.?SomeMethod ()" จะหมายถึง "เรียก SomeMethod ถ้า obj ไม่เป็นโมฆะมิฉะนั้นจะคืนค่า null" น่าเสียดายที่มันไม่พอดีกับงบประมาณของเราเราจึงไม่เคยนำมาใช้
Eric Lippert

@ เอริก: ความคิดเห็นนี้ยังใช้ได้อยู่ไหม? ฉันเคยเห็นบางที่มันสามารถใช้ได้กับ 4.0?
CharithJ

@CharithJ: ไม่. มันไม่เคยถูกนำมาใช้
Eric Lippert

3
@CharithJ: ฉันตระหนักถึงการมีอยู่ของตัวดำเนินการรวมกันเป็นโมฆะ มันไม่ได้ทำตามที่ดาร์ ธ ต้องการ เขาต้องการตัวดำเนินการเข้าถึงสมาชิกแบบยกไปเป็นโมฆะได้ (และโดยวิธีการที่ความคิดเห็นก่อนหน้าของคุณให้ลักษณะที่ไม่ถูกต้องของตัวดำเนินการการรวมกันเป็นโมฆะคุณหมายถึงการพูดว่า "v = m == null? y: m.Value สามารถเขียนได้ v = m ?? y")
Eric Lippert

4
สำหรับผู้อ่านรุ่นใหม่: C # 6.0 ใช้?. ดังนั้น x? .y? .z? .oString () จะคืนค่า null ถ้า x, y หรือ z เป็นโมฆะหรือจะส่งกลับ z ToString () หากไม่มีสิ่งใดเป็นโมฆะ
เดวิด

คำตอบ:


166

ตั้งแต่ C # 6 เป็นต้นไปคุณสามารถใช้:

MyEvent?.Invoke();

หรือ:

obj?.SomeMethod();

?.เป็นผู้ประกอบการ null แพร่กระจายและจะทำให้เกิดการที่จะลัดวงจรเมื่อถูกดำเนินการคือ.Invoke() nullมีการเข้าถึงตัวถูกดำเนินการเพียงครั้งเดียวดังนั้นจึงไม่มีความเสี่ยงที่จะเกิดปัญหา "การเปลี่ยนแปลงค่าระหว่างการตรวจสอบและเรียกใช้"

===

ก่อนหน้า C # 6 ไม่: ไม่มีเวทย์มนตร์ที่ปลอดภัยโดยมีข้อยกเว้นหนึ่งข้อ วิธีการขยาย - ตัวอย่างเช่น:

public static void SafeInvoke(this Action action) {
    if(action != null) action();
}

ตอนนี้ใช้ได้แล้ว:

Action act = null;
act.SafeInvoke(); // does nothing
act = delegate {Console.WriteLine("hi");}
act.SafeInvoke(); // writes "hi"

ในกรณีของเหตุการณ์นี้มีข้อดีของการลบเงื่อนไขการแข่งขันเช่นคุณไม่ต้องการตัวแปรชั่วคราว โดยปกติคุณจะต้อง:

var handler = SomeEvent;
if(handler != null) handler(this, EventArgs.Empty);

แต่ด้วย:

public static void SafeInvoke(this EventHandler handler, object sender) {
    if(handler != null) handler(sender, EventArgs.Empty);
}

เราสามารถใช้เพียง:

SomeEvent.SafeInvoke(this); // no race condition, no null risk

1
'มาขัดแย้งกันเล็กน้อยเกี่ยวกับเรื่องนี้ ในกรณีของตัวจัดการการดำเนินการหรือเหตุการณ์ - ซึ่งโดยปกติแล้วจะมีความเป็นอิสระมากกว่า - สิ่งนี้สมเหตุสมผล ฉันจะไม่ทำลายการห่อหุ้มด้วยวิธีปกติ นี่หมายถึงการสร้างวิธีการในคลาสแบบคงที่แยกต่างหากและฉันไม่คิดว่าการสูญเสียการห่อหุ้มและการย่อยสลายการอ่าน / การจัดระเบียบรหัสโดยรวมจะคุ้มค่ากับการปรับปรุงเล็กน้อยในการอ่านในท้องถิ่น
tvanfosson

@tvanfosson - แน่นอน; แต่ประเด็นของฉันคือนี่เป็นกรณีเดียวที่ฉันรู้ว่ามันจะทำงานที่ไหน และคำถามนั้นทำให้เกิดหัวข้อของผู้ได้รับมอบหมาย / กิจกรรม
Marc Gravell

รหัสนั้นจบลงด้วยการสร้างเมธอดที่ไม่ระบุชื่อที่ใดที่หนึ่งซึ่งจะทำให้การติดตามสแต็กของข้อยกเว้นนั้นยุ่งเหยิง เป็นไปได้ไหมที่จะตั้งชื่อเมธอดแบบไม่ระบุตัวตน ;)
sisve

@MarcGravell ไม่ใช่สิ่งนี้เรียกว่าตัวดำเนินการเงื่อนไขว่าง
RayLoveless

1
@mercu ควรจะเป็น?.- ใน VB14 ขึ้นไป
Marc Gravell

27

สิ่งที่คุณกำลังมองหาเป็นNull-เงื่อนไข (ไม่ใช่ "หลอมรวม") ?.ผู้ประกอบการ: มีให้ตั้งแต่ C # 6

obj?.SomeMethod();ตัวอย่างของคุณจะ ถ้า obj เป็นโมฆะจะไม่มีอะไรเกิดขึ้น เมื่อเมธอดมีอาร์กิวเมนต์เช่นobj?.SomeMethod(new Foo(), GetBar());อาร์กิวเมนต์จะไม่ถูกประเมินว่าobjเป็นโมฆะซึ่งมีความสำคัญหากการประเมินอาร์กิวเมนต์จะมีผลข้างเคียง

และการผูกมัดเป็นไปได้: myObject?.Items?[0]?.DoSomething()


1
นี่มันสุดยอดมาก เป็นที่น่าสังเกตว่านี่เป็นคุณลักษณะ C # 6 ... (ซึ่งจะบอกเป็นนัยจากคำสั่ง VS2015 ของคุณ แต่ก็ยังน่าสังเกต) :)
Kyle Goode

10

วิธีการขยายอย่างรวดเร็ว:

    public static void IfNotNull<T>(this T obj, Action<T> action, Action actionIfNull = null) where T : class {
        if(obj != null) {
            action(obj);
        } else if ( actionIfNull != null ) {
            actionIfNull();
        }
    }

ตัวอย่าง:

  string str = null;
  str.IfNotNull(s => Console.Write(s.Length));
  str.IfNotNull(s => Console.Write(s.Length), () => Console.Write("null"));

หรืออีกทางหนึ่ง:

    public static TR IfNotNull<T, TR>(this T obj, Func<T, TR> func, Func<TR> ifNull = null) where T : class {
        return obj != null ? func(obj) : (ifNull != null ? ifNull() : default(TR));
    }

ตัวอย่าง:

    string str = null;
    Console.Write(str.IfNotNull(s => s.Length.ToString());
    Console.Write(str.IfNotNull(s => s.Length.ToString(), () =>  "null"));

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

4

เหตุการณ์สามารถเริ่มต้นด้วยตัวแทนเริ่มต้นที่ว่างเปล่าซึ่งจะไม่ถูกลบออก:

public event EventHandler MyEvent = delegate { };

ไม่จำเป็นต้องตรวจสอบค่าว่าง

[ อัปเดตขอบคุณ Bevan ที่ชี้ให้เห็น]

โปรดทราบถึงผลกระทบด้านประสิทธิภาพที่อาจเกิดขึ้น การเปรียบเทียบขนาดเล็กอย่างรวดเร็วที่ฉันระบุบ่งชี้ว่าการจัดการกิจกรรมที่ไม่มีสมาชิกนั้นช้าลง 2-3 เท่าเมื่อใช้รูปแบบ "ตัวแทนเริ่มต้น" (บนแล็ปท็อป 2.5GHz แบบดูอัลคอร์ของฉันนั่นหมายถึง 279ms: 785ms สำหรับการเพิ่ม 50 ล้านเหตุการณ์ที่ไม่ได้สมัคร) สำหรับแอปพลิเคชันฮอตสปอตนั่นอาจเป็นปัญหาที่ต้องพิจารณา


1
ดังนั้นคุณจึงหลีกเลี่ยงการตรวจสอบค่าว่างโดยการเรียกผู้รับมอบสิทธิ์ที่ว่างเปล่า ... การเสียสละทั้งหน่วยความจำและเวลาที่วัดได้เพื่อประหยัดการกดแป้นพิมพ์เพียงไม่กี่ครั้ง? YMMV แต่สำหรับฉันการแลกเปลี่ยนที่ไม่ดี
Bevan

ฉันยังเห็นการวัดประสิทธิภาพที่แสดงว่ามีราคาแพงกว่ามากในการเรียกใช้งานกิจกรรมที่มีผู้ได้รับมอบหมายหลายคนสมัครเข้าร่วมมากกว่าเพียงรายการเดียว
Greg D


2

นี้บทความโดยเอียน Griffiths ให้สองโซลูชั่นที่แตกต่างกันในการแก้ไขปัญหาที่เขาสรุปเทคนิคอย่างที่คุณไม่ควรใช้


3
และในฐานะผู้เขียนบทความนั้นฉันขอเสริมว่าตอนนี้คุณไม่ควรใช้มันอย่างแน่นอนเพราะ C # 6 แก้ปัญหานี้นอกกรอบด้วยตัวดำเนินการเงื่อนไขว่าง (?. และ. [])
Ian Griffiths

2

วิธีการยุติการขยายอย่างที่แนะนำไม่ได้ช่วยแก้ปัญหาเกี่ยวกับสภาพการแข่งขันได้จริง ๆ แต่เป็นการซ่อนไว้

public static void SafeInvoke(this EventHandler handler, object sender)
{
    if (handler != null) handler(sender, EventArgs.Empty);
}

ตามที่ระบุไว้รหัสนี้เทียบเท่ากับโซลูชันที่มีตัวแปรชั่วคราว แต่ ...

ปัญหาที่เกิดขึ้นกับทั้งคู่อาจเป็นไปได้ว่าสามารถเรียก subsciber ของเหตุการณ์ได้หลังจากยกเลิกการสมัครรับข้อมูลจากเหตุการณ์แล้ว สิ่งนี้เป็นไปได้เนื่องจากการยกเลิกการสมัครสามารถเกิดขึ้นได้หลังจากคัดลอกอินสแตนซ์ของผู้ร่วมประชุมไปยังตัวแปร temp (หรือส่งผ่านเป็นพารามิเตอร์ในวิธีการด้านบน) แต่ก่อนที่จะเรียกใช้ตัวแทน

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

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

เพื่อให้การล็อกแม่นยำยิ่งขึ้นควรใช้กับอ็อบเจ็กต์การซิงค์เดียวกันกับที่ใช้ในวิธีการ add \ remove event accessor ซึ่งเป็นค่าเริ่มต้น "this"


นี่ไม่ใช่เงื่อนไขการแข่งขันที่อ้างถึง เงื่อนไขการแข่งขันคือ if (MyEvent! = null) // MyEvent ไม่เป็นโมฆะตอนนี้ MyEvent.Invoke (); // MyEvent เป็นโมฆะสิ่งที่ไม่ดีเกิดขึ้นดังนั้นด้วยเงื่อนไขการแข่งขันนี้ฉันไม่สามารถเขียนตัวจัดการเหตุการณ์ async ที่รับประกันว่าจะทำงานได้ อย่างไรก็ตามด้วย 'สภาพการแข่งขัน' ของคุณฉันสามารถเขียนตัวจัดการเหตุการณ์แบบ async ที่รับประกันว่าจะทำงานได้ แน่นอนว่าการโทรไปยังผู้สมัครสมาชิกและผู้ยกเลิกการสมัครต้องเป็นแบบซิงโครนัสหรือต้องมีรหัสล็อคที่นั่น
jyoung

แน่นอน. การวิเคราะห์ของฉันเกี่ยวกับปัญหานี้ถูกโพสต์ไว้ที่นี่: blogs.msdn.com/ericlippert/archive/2009/04/29/…
Eric Lippert


1

อาจจะไม่ดีกว่า แต่ในความคิดของฉันอ่านได้มากขึ้นคือการสร้างวิธีการขยาย

public static bool IsNull(this object obj) {
 return obj == null;
}

1
สิ่งที่ไม่return obj == nullหมายถึง สิ่งที่จะกลับมา
Hammad Khan

1
หมายความว่าถ้าobjเป็นnullวิธีการจะกลับมาtrueฉันคิดว่า
Joel

1
สิ่งนี้ตอบคำถามได้อย่างไร?
Kugel

ตอบช้า แต่แน่ใจหรือว่าจะได้ผล คุณสามารถเรียกวิธีการขยายบนวัตถุที่null? ฉันค่อนข้างแน่ใจว่าวิธีนี้ใช้ไม่ได้กับวิธีการของ type ดังนั้นฉันสงสัยว่ามันจะใช้ได้กับวิธีการขยายเช่นกัน ผมเชื่อว่าวิธีที่ดีที่สุดเพื่อตรวจสอบว่าวัตถุเป็น ถูกnull obj is nullน่าเสียดายที่การตรวจสอบว่าวัตถุนั้นไม่nullจำเป็นต้องมีการห่อในวงเล็บซึ่งเป็นเรื่องที่โชคร้าย
natiiix

0

ฉันได้สร้างส่วนขยายทั่วไปที่ฉันใช้

public static class ObjectExtensions {
    public static void With<T>(this T value, Action<T> todo) {
        if (value != null) todo(value);
    }
}

จากนั้นฉันใช้มันตามด้านล่าง

string myString = null;
myString.With((value) => Console.WriteLine(value)); // writes nothing
myString = "my value";
myString.With((value) => Console.WriteLine(value)); // Writes `my value`

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