C # วิธีที่สวยงามในการตรวจสอบว่าคุณสมบัติของคุณสมบัติเป็นโมฆะหรือไม่


100

ใน C # บอกว่าคุณต้องการดึงค่าออกจาก PropertyC ในตัวอย่างนี้และ ObjectA, PropertyA และ PropertyB ทั้งหมดสามารถเป็นค่าว่างได้

ObjectA.PropertyA.PropertyB.PropertyC

ฉันจะรับ PropertyC อย่างปลอดภัยโดยใช้รหัสน้อยที่สุดได้อย่างไร?

ตอนนี้ฉันจะตรวจสอบ:

if(ObjectA != null && ObjectA.PropertyA !=null && ObjectA.PropertyA.PropertyB != null)
{
    // safely pull off the value
    int value = objectA.PropertyA.PropertyB.PropertyC;
}

มันจะเป็นการดีที่จะทำอะไรแบบนี้ (รหัสหลอก)

int value = ObjectA.PropertyA.PropertyB ? ObjectA.PropertyA.PropertyB : defaultVal;

อาจยุบลงไปอีกด้วยตัวดำเนินการเชื่อมต่อแบบ null

แก้ไขเดิมทีฉันบอกว่าตัวอย่างที่สองของฉันเป็นเหมือน js แต่ฉันเปลี่ยนเป็นรหัส psuedo เนื่องจากมีการชี้ให้เห็นอย่างถูกต้องว่ามันจะไม่ทำงานใน js


ฉันไม่เห็นว่าตัวอย่าง js ของคุณทำงานอย่างไร คุณควรได้รับข้อผิดพลาด "คาดว่าวัตถุ" เมื่อใดก็ตามObjectAหรือPropertyAเป็นโมฆะ
lincolnk

คำตอบ:


124

ใน C # 6 คุณสามารถใช้Null เงื่อนไขประกอบการ ดังนั้นการทดสอบดั้งเดิมจะเป็น:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;

2
คุณช่วยอธิบายว่ามันทำอะไรได้บ้าง? อะไรคือสิ่งที่valueมีค่าเท่ากับถ้าPropertyCเป็นโมฆะ? หรือถ้าPropertyBเป็นโมฆะ? เกิดอะไรขึ้นถ้าObject Aเป็นโมฆะ?
Kolob Canyon

1
nullถ้าส่วนใดของคุณสมบัติเหล่านี้เป็นโมฆะแล้วผลตอบแทนที่คำสั่งทั้งหมดเป็น เริ่มจากซ้ายไปขวา หากไม่มีน้ำตาลสังเคราะห์สิ่งนี้จะเทียบเท่ากับชุดของคำสั่ง if ที่if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......มีการแสดงออกสุดท้ายในที่สุดif(propertyZ != null) { value = propertyZ }
DetectivePikachu

27

วิธีการต่อแบบสั้น:

public static TResult IfNotNull<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator)
  where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

การใช้

PropertyC value = ObjectA.IfNotNull(x => x.PropertyA).IfNotNull(x => x.PropertyB).IfNotNull(x => x.PropertyC);

วิธีการขยายแบบง่ายๆนี้และอื่น ๆ อีกมากมายที่คุณสามารถหาได้จากhttp://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/

แก้ไข:

หลังจากใช้งานไปสักครู่ฉันคิดว่าชื่อที่เหมาะสมสำหรับวิธีนี้ควรเป็นIfNotNull ()แทนที่จะเป็น With ()


15

คุณสามารถเพิ่มวิธีการในชั้นเรียนของคุณได้หรือไม่? ถ้าไม่คุณเคยคิดจะใช้วิธีการขยายหรือไม่? GetPropC()คุณสามารถสร้างวิธีขยายสำหรับประเภทของวัตถุที่เรียกว่า

ตัวอย่าง:

public static class MyExtensions
{
    public static int GetPropC(this MyObjectType obj, int defaltValue)
    {
        if (obj != null && obj.PropertyA != null & obj.PropertyA.PropertyB != null)
            return obj.PropertyA.PropertyB.PropertyC;
        return defaltValue;
    }
}

การใช้งาน:

int val = ObjectA.GetPropC(0); // will return PropC value, or 0 (defaltValue)

โดยวิธีนี้ถือว่าคุณกำลังใช้. NET 3 หรือสูงกว่า


11

วิธีที่คุณทำนั้นถูกต้อง

คุณสามารถใช้เคล็ดลับเช่นเดียวกับที่อธิบายไว้ที่นี่โดยใช้นิพจน์ Linq:

int value = ObjectA.NullSafeEval(x => x.PropertyA.PropertyB.PropertyC, 0);

แต่ช้ากว่ามากที่ตรวจสอบคุณสมบัติแต่ละรายการด้วยตนเอง ...


10

Refactor เพื่อปฏิบัติตามกฎของ Demeter


ฉันไม่พิจารณาว่ากราฟออบเจ็กต์มีความลึกเพียงสามระดับเท่านั้นที่จำเป็นต้องปรับโครงสร้างใหม่เมื่อคุณอ่านคุณสมบัติเท่านั้น ฉันเห็นด้วยหาก OP ต้องการเรียกใช้เมธอดบนวัตถุที่อ้างอิงผ่าน PropertyC แต่ไม่ใช่เมื่อเป็นคุณสมบัติที่ต้องตรวจสอบค่าว่างก่อนอ่าน ในตัวอย่างนี้อาจทำได้ง่าย ๆ เช่น Customer.Address.Country โดยที่ Country อาจเป็นประเภทอ้างอิงเช่น KeyValuePair คุณจะ refactor สิ่งนี้อย่างไรเพื่อป้องกันความจำเป็นในการตรวจสอบการอ้างอิงว่าง
Darren Lewis

ตัวอย่าง OP มีความลึก 4 จริง ข้อเสนอแนะของฉันไม่ได้เป็นการลบการตรวจสอบการอ้างอิงว่าง แต่เป็นการค้นหาในวัตถุที่น่าจะจัดการได้อย่างเหมาะสม เช่นเดียวกับ "กฎง่ายๆ" ส่วนใหญ่มีข้อยกเว้น แต่ฉันไม่มั่นใจว่านี่เป็นข้อเดียว เราสามารถตกลงที่จะไม่เห็นด้วยได้หรือไม่?
rtalbot

4
ฉันเห็นด้วยกับ @rtalbot (แม้ว่าในความเป็นธรรม @Daz Lewis กำลังเสนอตัวอย่าง 4 ระดับลึกเนื่องจากรายการสุดท้ายคือ KeyValuePair) หากมีบางอย่างยุ่งกับวัตถุของลูกค้าฉันจะไม่เห็นว่าธุรกิจนั้นมีธุรกิจอะไรบ้างในการพิจารณามรดกของวัตถุที่อยู่ สมมติว่าคุณตัดสินใจในภายหลังว่า KeyValuePair ไม่ใช่ความคิดที่ดีสำหรับคุณสมบัติของประเทศ ในกรณีนี้รหัสของทุกคนต้องเปลี่ยน นั่นไม่ใช่การออกแบบที่ดี
Jeffrey L Whitledge

10

อัปเดต 2014: C # 6 มีตัวดำเนินการใหม่ที่?.เรียกว่า 'การนำทางที่ปลอดภัย' หรือ 'การแพร่สัญญาณว่าง'

parent?.child

อ่านhttp://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/at-last-c-is-getting-sometimes-called-the-safe-navigation-operator.aspxสำหรับรายละเอียด

นี่เป็นคำขอที่ได้รับความนิยมอย่างมาก https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3990187-add-operator-to-c-?tracking_code=594c10a522f8e9bc987ee4a5e2c0b38d


8

เห็นได้ชัดว่าคุณกำลังมองหาNullable Monad :

string result = new A().PropertyB.PropertyC.Value;

กลายเป็น

string result = from a in new A()
                from b in a.PropertyB
                from c in b.PropertyC
                select c.Value;

สิ่งนี้จะส่งคืนnullหากคุณสมบัติที่เป็นโมฆะใด ๆ เป็นโมฆะ มิฉะนั้นค่าของValue.

class A { public B PropertyB { get; set; } }
class B { public C PropertyC { get; set; } }
class C { public string Value { get; set; } }

วิธีการขยาย LINQ:

public static class NullableExtensions
{
    public static TResult SelectMany<TOuter, TInner, TResult>(
        this TOuter source,
        Func<TOuter, TInner> innerSelector,
        Func<TOuter, TInner, TResult> resultSelector)
        where TOuter : class
        where TInner : class
        where TResult : class
    {
        if (source == null) return null;
        TInner inner = innerSelector(source);
        if (inner == null) return null;
        return resultSelector(source, inner);
    }
}

ทำไมวิธีการขยายจึงอยู่ที่นี่? มันไม่ได้ถูกใช้
Mladen Mihajlovic

1
@MladenMihajlovic: ไวยากรณ์SelectManyใช้วิธีการขยาย from ... in ... from ... in ...
dtb

5

สมมติว่าคุณมีค่าที่ว่างเปล่าของวิธีการประเภทหนึ่งจะเป็นดังนี้:

var x = (((objectA ?? A.Empty).PropertyOfB ?? B.Empty).PropertyOfC ?? C.Empty).PropertyOfString;

ฉันเป็นแฟนตัวยงของ C # แต่สิ่งที่ดีมากใน Java ใหม่ (1.7?) คือ.? ผู้ดำเนินการ:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;

1
มันจะอยู่ใน Java 1.7 จริงหรือ? ได้รับการร้องขอใน C # เป็นเวลานาน แต่ฉันสงสัยว่ามันจะเกิดขึ้น ...
Thomas Levesque

น่าเสียดายที่ฉันไม่มีค่าว่างเปล่า ไวยากรณ์ Java นั้นดูน่ารัก! ฉันจะโหวตสิ่งนี้เพียงเพราะฉันต้องการไวยากรณ์นั้น!
จอนคราห์

3
Thomas: ครั้งสุดท้ายที่ฉันตรวจสอบtech.puredanger.com/java7มันบอกเป็นนัยว่า Java จะได้รับมัน แต่ตอนนี้เมื่อฉันตรวจสอบอีกครั้งมันบอกว่า: Null safe handle: NO. ดังนั้นฉันจึงเพิกถอนคำสั่งของฉันและแทนที่ด้วยคำสั่งใหม่: มีการเสนอสำหรับ Java 1.7 แต่ไม่ได้ทำ
metaprogrammer คนอื่นเมื่อ

วิธีการเพิ่มเติมคือวิธีที่ monad.net ใช้
เพียงแค่ metaprogrammer อื่น

1
ดูเหมือนว่า?. ผู้ดำเนินการอยู่ใน Visual Studio 2015 https://msdn.microsoft.com/en-us/library/dn986595.aspx
เอ็ดเวิร์ด

4

รหัสนี้เป็น "รหัสจำนวนน้อยที่สุด" แต่ไม่ใช่แนวทางปฏิบัติที่ดีที่สุด:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}

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

บางครั้งมันก็น่าขบขันที่ได้อ่านคำตอบของตัวเองหลังจากผ่านไป 3 ปี ฉันคิดว่าวันนี้ฉันจะตอบต่างออกไป ฉันจะบอกว่ารหัสนั้นละเมิดกฎหมายของ Demeter และฉันขอแนะนำให้ปรับโครงสร้างใหม่เพื่อที่จะไม่ทำเช่นนั้น
Boris Modylevsky

1
ณ วันนี้ 7 ปีหลังจากคำตอบเดิมฉันจะเข้าร่วม @Phillip Ngan และจะใช้ C # 6 กับไวยากรณ์ต่อไปนี้: int? ค่า = objectA? .PropertyA? .PropertyB? .PropertyC;
Boris Modylevsky

4

เมื่อฉันต้องการเชื่อมต่อการโทรแบบนั้นฉันอาศัยวิธีการช่วยเหลือที่ฉันสร้างขึ้น TryGet ():

    public static U TryGet<T, U>(this T obj, Func<T, U> func)
    {
        return obj.TryGet(func, default(U));
    }

    public static U TryGet<T, U>(this T obj, Func<T, U> func, U whenNull)
    {
        return obj == null ? whenNull : func(obj);
    }

ในกรณีของคุณคุณจะใช้มันดังนี้:

    int value = ObjectA
        .TryGet(p => p.PropertyA)
        .TryGet(p => p.PropertyB)
        .TryGet(p => p.PropertyC, defaultVal);

ฉันไม่คิดว่ารหัสนี้ใช้งานได้ defaultVal ประเภทใด var p = บุคคลใหม่ (); Assert.AreEqual (p.TryGet (x => x.FirstName) .TryGet (x => x.LastName) .TryGet (x => x.NickName, "foo"), "foo");
Keith

ตัวอย่างที่ฉันเขียนควรอ่านเช่น ObjectA.PropertyA.PropertyB.PropertyC ดูเหมือนว่าโค้ดของคุณกำลังพยายามโหลดคุณสมบัติที่เรียกว่า "LastName" จาก "FirstName" ซึ่งไม่ใช่การใช้งานที่ตั้งใจไว้ ตัวอย่างที่ถูกต้องจะเป็นดังนี้: var postcode = person.TryGet (p => p.Address) .TryGet (p => p.Postcode); อย่างไรก็ตามวิธีการช่วยเหลือ TryGet () ของฉันคล้ายกับคุณลักษณะใหม่ใน C # 6.0 ซึ่งเป็นตัวดำเนินการที่มีเงื่อนไขว่างเปล่า ลักษณะการใช้งานจะเป็นดังนี้: var postcode = person? .Address? .Postcode; msdn.microsoft.com/en-us/magazine/dn802602.aspx
Emanuel

3

ฉันเห็นบางอย่างใน C # 6.0 ใหม่โดยใช้ '?' แทนที่จะตรวจสอบ

ตัวอย่างเช่นแทนที่จะใช้

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)
{ 
  var city = person.contact.address.city;
}

คุณเพียงแค่ใช้

var city = person?.contact?.address?.city;

ฉันหวังว่ามันจะช่วยใครสักคน


อัพเดท:

คุณสามารถทำเช่นนี้ได้ในขณะนี้

 var city = (Person != null)? 
           ((Person.Contact!=null)? 
              ((Person.Contact.Address!= null)?
                      ((Person.Contact.Address.City!=null)? 
                                 Person.Contact.Address.City : null )
                       :null)
               :null)
            : null;

1

คุณสามารถทำได้:

class ObjectAType
{
    public int PropertyC
    {
        get
        {
            if (PropertyA == null)
                return 0;
            if (PropertyA.PropertyB == null)
                return 0;
            return PropertyA.PropertyB.PropertyC;
        }
    }
}



if (ObjectA != null)
{
    int value = ObjectA.PropertyC;
    ...
}

หรือดีกว่านั้นอาจเป็นสิ่งนี้:

private static int GetPropertyC(ObjectAType objectA)
{
    if (objectA == null)
        return 0;
    if (objectA.PropertyA == null)
        return 0;
    if (objectA.PropertyA.PropertyB == null)
        return 0;
    return objectA.PropertyA.PropertyB.PropertyC;
}


int value = GetPropertyC(ObjectA);

1

คุณสามารถใช้ส่วนขยายต่อไปนี้และฉันคิดว่ามันดีจริงๆ:

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<TF, TR>(TF t, Func<TF, TR> f)
    where TF : class
{
    return t != null ? f(t) : default(TR);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, TR>(T1 p1, Func<T1, T2> p2, Func<T2, TR> p3)
    where T1 : class
    where T2 : class
{
    return Get(Get(p1, p2), p3);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, T3, TR>(T1 p1, Func<T1, T2> p2, Func<T2, T3> p3, Func<T3, TR> p4)
    where T1 : class
    where T2 : class
    where T3 : class
{
    return Get(Get(Get(p1, p2), p3), p4);
}

และใช้ในลักษณะนี้:

int value = Nulify.Get(objectA, x=>x.PropertyA, x=>x.PropertyB, x=>x.PropertyC);

0

ฉันจะเขียนวิธีของคุณเองในประเภท PropertyA (หรือวิธีการขยายหากไม่ใช่ประเภทของคุณ) โดยใช้รูปแบบที่คล้ายกันกับประเภท Nullable

class PropertyAType
{
   public PropertyBType PropertyB {get; set; }

   public PropertyBType GetPropertyBOrDefault()
   {
       return PropertyB != null ? PropertyB : defaultValue;
   }
}

ในกรณีนั้นเห็นได้ชัดว่า PropertyB ไม่สามารถเป็นโมฆะได้
เวียนซ้ำ

0

เพิ่งสะดุดในโพสต์นี้

เมื่อไม่นานมานี้ฉันได้ให้คำแนะนำเกี่ยวกับ Visual Studio Connect เกี่ยวกับการเพิ่มไฟล์ ???ดำเนินการ

http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4104392-add-as-an-recursive-null-reference-check-opera

สิ่งนี้ต้องการการทำงานบางอย่างจากทีมเฟรมเวิร์ก แต่ไม่จำเป็นต้องเปลี่ยนภาษา แต่เพียงแค่ทำเวทมนตร์ของคอมไพเลอร์ แนวคิดคือคอมไพเลอร์ควรเปลี่ยนรหัสนี้ (ไม่อนุญาตให้ใช้ไวยากรณ์ atm)

string product_name = Order.OrderDetails[0].Product.Name ??? "no product defined";

ลงในรหัสนี้

Func<string> _get_default = () => "no product defined"; 
string product_name = Order == null 
    ? _get_default.Invoke() 
    : Order.OrderDetails[0] == null 
        ? _get_default.Invoke() 
        : Order.OrderDetails[0].Product == null 
            ? _get_default.Invoke() 
            : Order.OrderDetails[0].Product.Name ?? _get_default.Invoke()

สำหรับการตรวจสอบค่าว่างสิ่งนี้อาจมีลักษณะดังนี้

bool isNull = (Order.OrderDetails[0].Product ??? null) == null;

0

ฉันเขียนวิธีการที่ยอมรับค่าเริ่มต้นนี่คือวิธีใช้:

var teacher = new Teacher();
return teacher.GetProperty(t => t.Name);
return teacher.GetProperty(t => t.Name, "Default name");

นี่คือรหัส:

public static class Helper
{
    /// <summary>
    /// Gets a property if the object is not null.
    /// var teacher = new Teacher();
    /// return teacher.GetProperty(t => t.Name);
    /// return teacher.GetProperty(t => t.Name, "Default name");
    /// </summary>
    public static TSecond GetProperty<TFirst, TSecond>(this TFirst item1,
        Func<TFirst, TSecond> getItem2, TSecond defaultValue = default(TSecond))
    {
        if (item1 == null)
        {
            return defaultValue;
        }

        return getItem2(item1);
    }
}

1
วิธีนี้ได้ระบุไว้ในคำตอบอื่น ๆ แล้ว (ซ้ำ ๆ ) มีเหตุผลที่ทุกคนจะได้รับการโพสต์มันไม่มีอีกครั้ง
Servy

ฉันไม่เห็นว่ามีการใช้ค่าเริ่มต้น
Akira Yamamoto

ฉันนับอีก 6 คนที่ใช้ค่าเริ่มต้นที่กำหนดไว้ เห็นได้ชัดว่าคุณไม่ได้ดูยากเลย
Servy

0

มันเป็นไปไม่ได้.
ObjectA.PropertyA.PropertyBจะล้มเหลวหากObjectAเป็นโมฆะเนื่องจากการอ้างอิงเป็นโมฆะซึ่งเป็นข้อผิดพลาด

if(ObjectA != null && ObjectA.PropertyA... ทำงานเนื่องจากการลัดวงจรกล่าวคือObjectA.PropertyAจะไม่ถูกตรวจสอบว่าObjectAเป็นnullเป็น

วิธีแรกที่คุณเสนอดีที่สุดและชัดเจนที่สุดด้วยเจตนา หากมีสิ่งใดที่คุณสามารถลองออกแบบใหม่โดยไม่ต้องพึ่งพาค่าว่างมากมาย


-1

วิธีนี้ค่อนข้างตรงไปตรงมาเมื่อคุณเอาชนะ lambda gobbly-gook:

public static TProperty GetPropertyOrDefault<TObject, TProperty>(this TObject model, Func<TObject, TProperty> valueFunc)  
                                                        where TObject : class
    {
        try
        {
            return valueFunc.Invoke(model);
        }
        catch (NullReferenceException nex)
        {
            return default(TProperty);
        }
    }

ด้วยการใช้งานที่อาจมีลักษณะดังนี้:

ObjectA objectA = null;

Assert.AreEqual(0,objectA.GetPropertyOrDefault(prop=>prop.ObjectB.ObjectB.ObjectC.ID));

Assert.IsNull(objectA.GetPropertyOrDefault(prop => prop.ObjectB));

แค่อยากรู้ว่าทำไมมีคนโหวตลงคะแนน 8 ปีหลังจากที่ฉันให้คำตอบ (ซึ่งเป็นปีก่อนที่ C # 6 จะรวมกันเป็นโมฆะ)
BlackjacketMack

-3
var result = nullableproperty ?? defaultvalue;

??(ค่า null coalescing ดำเนินการ) หมายถึงถ้าอาร์กิวเมนต์แรกคือnullกลับมาคนที่สองแทน


2
คำตอบนี้ไม่ได้ช่วยแก้ปัญหาของ OP คุณจะใช้วิธีแก้ปัญหาอย่างไร ?? ตัวดำเนินการกับ ObjectA.PropertyA.PropertyB เมื่อทุกส่วนของนิพจน์ (ObjectA, PropertyA และ PropertyB) สามารถเป็นโมฆะได้?
Artemix

จริงฉันคิดว่าฉันไม่ได้อ่านคำถามเลยด้วยซ้ำ อย่างไรก็ตามไม่มีอะไรเป็นไปไม่ได้ก็อย่าทำ: P คงโมฆะ Main (สตริง [] args) {a ca = new a (); var default_value = ใหม่ a () {b = วัตถุใหม่ ()}; ค่า var = (ca ?? default_value) .b ?? default_value.b; } คลาส a {public object b = null; }
Aridane Álamo

(ObjectA ?? DefaultMockedAtNull) .PropertyA! = null? ObjectA.PropertyA.PropertyB: null
Aridane Álamo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.