Derin boşluk kontrolü, daha iyi bir yol var mı?


130

Not: Bu soru giriş önce istendi Studio 2015 6. / Visual C operatörü ..?

Hepimiz oradaydık, cake.frosting.berries.loader gibi derin bir özelliğimiz var, boş olup olmadığını kontrol etmemiz gerekiyor, bu yüzden istisna yok. Bunu yapmanın yolu, kısa devre yapan if ifadesi kullanmaktır.

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...

Bu tam olarak zarif değildir ve belki de tüm zinciri kontrol etmenin ve boş bir değişken / özelliğe karşı çıkıp çıkmadığını görmenin daha kolay bir yolu olmalıdır.

Bir uzatma yöntemi kullanmak mümkün mü yoksa bir dil özelliği mi yoksa sadece kötü bir fikir mi?


3
Bunu sık sık diledim - ama ortaya attığım tüm fikirler gerçek sorundan daha kötüydü.
peterchen

Tüm cevaplar için teşekkürler ve diğer insanların da aynı düşüncelere sahip olduğunu görmek ilginç. Bunun nasıl çözüldüğünü kendim nasıl görmek istediğimi düşünmeliyim ve Eric'in çözümleri güzel olsa da, şöyle bir şey yazacağımı düşünüyorum: if (IsNull (abc)) veya if (IsNotNull (abc)) ama belki bu sadece benim zevkime göre :)
Homde

Donmayı örneklediğinizde, bir çilek özelliği vardır, bu nedenle kurucunuzun bu noktasında, frosting'e, ne zaman boş (boş olmayan) bir çilek yaratması için başlatıldığını söyleyebilir misiniz? ve meyveler ne zaman değiştirilirse, şekerleme değeri kontrol edilir ????
Doug Chamberlain

Biraz gevşek bir şekilde ilişkili, buradaki bazı teknikler, aşmaya çalıştığım "derin boşluklar" problemi için tercih edilebilir buldum. stackoverflow.com/questions/818642/…
AaronLS

Yanıtlar:


223

Yeni bir "?" Operasyonu eklemeyi düşündük. istediğiniz semantiğe sahip dile. (Ve şimdi eklendi; aşağıya bakın.) Yani,

cake?.frosting?.berries?.loader

ve derleyici sizin için tüm kısa devre kontrollerini oluşturacaktır.

C # 4 için çıtayı koymadı. Belki dilin varsayımsal bir gelecek versiyonu için.

Güncelleme (2014):?. operatör şimdi olduğu planlanan sonraki Roslyn derleyici serbest bırakılması için. Operatörün tam sözdizimsel ve anlambilimsel analizi konusunda hala bazı tartışmalar olduğunu unutmayın.

Güncelleme (Temmuz 2015): Visual Studio 2015 yayınlandı ve boş koşullu işleçleri?.?[] destekleyen bir C # derleyicisi ile birlikte ve .


10
Nokta olmadan, koşullu (A? B: C) operatörle sözdizimsel olarak belirsiz hale gelir. Jeton akışında keyfi olarak "ileriye bakmamızı" gerektiren sözcüksel yapılardan kaçınmaya çalışıyoruz. (Ne yazık ki, C # 'da zaten bu tür yapılar var; daha fazlasını eklememeyi tercih ediyoruz.)
Eric Lippert

33
@ Ian: Bu sorun son derece yaygındır. Bu, aldığımız en sık taleplerden biridir.
Eric Lippert

7
@Ian: Mümkün olduğunda boş nesne modelini kullanmayı da tercih ederim, ancak çoğu insan kendi tasarladıkları nesne modelleriyle çalışma lüksüne sahip değil. Mevcut nesne modellerinin çoğu null kullanır ve bu yüzden yaşamak zorunda olduğumuz dünya budur.
Eric Lippert

12
@John: Biz neredeyse tamamen bu özellik isteği olsun gelen en deneyimli programcılar. MVP'ler bunu her zaman istiyor . Ama fikirlerin değiştiğini anlıyorum; Eleştirinize ek olarak yapıcı bir dil tasarımı önerisi vermek isterseniz, bunu dikkate almaktan mutluluk duyarım.
Eric Lippert

28
@lazyberezovsky: Demeter'in sözde "yasasını" hiç anlamadım; her şeyden önce, daha doğru bir şekilde "Demeter'in Önerisi" olarak adlandırılıyor gibi görünüyor. Ve ikincisi, mantıksal sonucuna "yalnızca bir üye erişimi" almanın sonucu, müşterinin ne yapacağını bilen nesneleri dağıtmaktan ziyade, her nesnenin her müşteri için her şeyi yapması gereken "Tanrı nesneleri" dir. istiyor. Demeter yasasının tam tersini tercih ederim: her nesne az sayıda sorunu iyi çözer ve bu çözümlerden biri "sorununuzu daha iyi çözen başka bir nesne olabilir"
Eric Lippert

27

İfade ağaçları kullanılarak daha kolay / daha güzel bir sözdizimi ile bu tür derin boşluk kontrolünün nasıl yapılabileceğini öğrenmek için bu sorudan ilham aldım. Hiyerarşinin derinliklerindeki örneklere sık sık erişmeniz gerekiyorsa bunun kötü bir tasarım olabileceğini belirten yanıtlara katılıyorum, ancak veri sunumu gibi bazı durumlarda bunun çok yararlı olabileceğini düşünüyorum.

Bu yüzden yazmanıza izin verecek bir uzantı yöntemi oluşturdum:

var berries = cake.IfNotNull(c => c.Frosting.Berries);

İfadenin hiçbir bölümü boş değilse, bu Berries'i döndürecektir. Null ile karşılaşılırsa, null döndürülür. Yine de bazı uyarılar var, şu anki sürümde yalnızca basit üye erişimiyle çalışacak ve yalnızca .NET Framework 4'te çalışıyor çünkü v4.2'de yeni olan MemberExpression.Update yöntemini kullanıyor. Bu, IfNotNull uzantı yönteminin kodudur:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace dr.IfNotNullOperator.PoC
{
    public static class ObjectExtensions
    {
        public static TResult IfNotNull<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            if (ReferenceEquals(arg, null))
                return default(TResult);

            var stack = new Stack<MemberExpression>();
            var expr = expression.Body as MemberExpression;
            while(expr != null)
            {
                stack.Push(expr);
                expr = expr.Expression as MemberExpression;
            } 

            if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression))
                throw new ApplicationException(String.Format("The expression '{0}' contains unsupported constructs.",
                                                             expression));

            object a = arg;
            while(stack.Count > 0)
            {
                expr = stack.Pop();
                var p = expr.Expression as ParameterExpression;
                if (p == null)
                {
                    p = Expression.Parameter(a.GetType(), "x");
                    expr = expr.Update(p);
                }
                var lambda = Expression.Lambda(expr, p);
                Delegate t = lambda.Compile();                
                a = t.DynamicInvoke(a);
                if (ReferenceEquals(a, null))
                    return default(TResult);
            }

            return (TResult)a;            
        }
    }
}

İfadenizi temsil eden ifade ağacını inceleyerek ve parçaları birbiri ardına değerlendirerek çalışır; her seferinde sonucun boş olmadığını kontrol edin.

Eminim bu, MemberExpression dışındaki diğer ifadelerin desteklenmesi için uzatılabilir. Bunu kavram kanıtı kodu olarak düşünün ve lütfen bunu kullandığınızda bir performans cezası olacağını unutmayın (bu muhtemelen çoğu durumda önemli olmayacaktır, ancak sıkı bir döngü içinde kullanmayın :-))


Lambda becerilerinizden çok etkilendim :) Ancak sözdizimi, birinin istediğinden biraz daha karmaşık görünüyor, en azından if-ifadesi senaryosu için
Homde

Harika, ancak if .. &&'den 100 kat daha fazla kod gibi çalışıyor. Yalnızca hala bir if .. && olarak derlenirse faydalıdır.
Monstieur

1
Ah ve sonra DynamicInvokeorada gördüm . Bundan dini olarak kaçınıyorum :)
nawfal

24

Bu uzantıyı, derin yuvalama senaryoları için oldukça yararlı buldum.

public static R Coal<T, R>(this T obj, Func<T, R> f)
    where T : class
{
    return obj != null ? f(obj) : default(R);
}

Bu, C # ve T-SQL'deki boş birleştirme operatöründen çıkardığım bir fikir. Güzel olan şey, dönüş türünün her zaman iç özelliğin dönüş türü olmasıdır.

Bu şekilde bunu yapabilirsiniz:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);

... veya yukarıdakinin hafif bir varyasyonu:

var berries = cake.Coal(x => x.frosting, x => x.berries);

Bildiğim en iyi sözdizimi değil ama işe yarıyor.


Neden "Kömür", bu çok ürkütücü görünüyor. ;) Ancak, donma boşsa, numuneniz başarısız olur. Öyle görünmeliydi: var berry = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke

Oh, ama ikinci argümanın Kömür'e bir çağrı olmadığını ima ediyorsun ki, öyle olması gerekiyor. Bu sadece uygun bir değişiklik. Seçici (x => x.berries), iki argüman alan Coal yöntemi içindeki bir Kömür çağrısına geçirilir.
John Leidegren

Birleştirme veya birleştirme adı T-SQL'den alınmıştır, fikri ilk aldığım yer burasıdır. IfNotNull, boş değilse bir şeyin gerçekleştiğini belirtir, ancak bunun ne olduğu, IfNotNull yöntem çağrısı tarafından açıklanmamaktadır. Kömür gerçekten tuhaf bir isim, ancak bu aslında dikkate değer garip bir yöntem.
John Leidegren

Bunun için tam anlamıyla en iyi isim "ReturnIfNotNull" veya "ReturnOrDefault" gibi bir ad olabilir
John Leidegren

@flq 1 ... projemize o oluyor da :) IfNotNull denilen
Marc Sigrist

16

Demeter Yasasını ihlal etmenin yanı sıra, Mehrdad Afshari'nin daha önce de belirttiği gibi, bana öyle geliyor ki karar mantığı için "derin bir sıfır kontrolüne" ihtiyacınız var.

Bu, genellikle boş nesneleri varsayılan değerlerle değiştirmek istediğinizde ortaya çıkar. Bu durumda, Boş Nesne Kalıbını uygulamayı düşünmelisiniz . Varsayılan değerler ve "eylem dışı" yöntemler sağlayarak, gerçek bir nesne için bir stand-in görevi görür.


hayır, amaç-c boş nesnelere mesaj göndermeye izin verir ve gerekirse uygun varsayılan değeri döndürür. Orada sorun yok.
Johannes Rudolph

2
Evet. Konu bu. Temel olarak, ObjC davranışını Null Object Pattern ile taklit edeceksiniz.
Mehrdad Afshari

10

Güncelleme: Visual Studio 2015'ten başlayarak, C # derleyicisi (dil sürümü 6) artık ?.operatörü tanıyor ve bu da "derin boşluk denetimini" çocuk oyuncağı haline getiriyor. Ayrıntılar için bu yanıta bakın.

Bu silinmiş cevabın önerdiği gibi kodunuzu yeniden tasarlamanın yanı sıra , başka (korkunç da olsa) bir seçenek, bu derin mülk araması sırasında try…catchbir ara NullReferenceExceptionmeydana gelip gelmediğini görmek için bir blok kullanmak olabilir .

try
{
    var x = cake.frosting.berries.loader;
    ...
}
catch (NullReferenceException ex)
{
    // either one of cake, frosting, or berries was null
    ...
}

Şahsen bunu aşağıdaki nedenlerden dolayı yapmazdım:

  • Hoş görünmüyor.
  • Normal çalışma sırasında sık sık olmasını beklediğiniz bir şeyi değil, istisnai durumları hedeflemesi gereken istisna işlemeyi kullanır.
  • NullReferenceExceptions muhtemelen hiçbir zaman açıkça yakalanmamalıdır. ( Bu soruya bakın .)

Öyleyse, bazı uzatma yöntemlerini kullanmak mümkün mü yoksa bir dil özelliği mi, [...]

Bu neredeyse kesin (biçiminde C # 6 kullanılabilen bir dil özelliği olması gerekir .?ve ?[]C # zaten daha sofistike tembel muayeneden geçmiş sürece, operatörler) veya muhtemelen de değildir yansıması (kullanmak istemiyorsanız performans ve tip güvenliği açısından iyi bir fikir).

cake.frosting.berries.loaderBir işleve basitçe geçiş yapmanın bir yolu olmadığından (değerlendirilir ve boş bir referans istisnası atılır), aşağıdaki şekilde genel bir arama yöntemi uygulamanız gerekir: Bir nesneyi ve özelliklerin adlarını alır yukarı Bak:

static object LookupProperty( object startingPoint, params string[] lookupChain )
{
    // 1. if 'startingPoint' is null, return null, or throw an exception.
    // 2. recursively look up one property/field after the other from 'lookupChain',
    //    using reflection.
    // 3. if one lookup is not possible, return null, or throw an exception.
    // 3. return the last property/field's value.
}

...

var x = LookupProperty( cake, "frosting", "berries", "loader" );

(Not: kod düzenlendi.)

Böyle bir yaklaşımla hızlıca birkaç sorun görürsünüz. İlk olarak, herhangi bir güvenlik türü ve basit bir türdeki özellik değerlerinin olası kutulamasını elde edemezsiniz. İkincisi, bir nullşeyler ters giderse geri dönebilirsiniz ve arama işlevinizde bunu kontrol etmeniz gerekecek veya bir istisna atarsınız ve başladığınız yere geri dönersiniz. Üçüncüsü, yavaş olabilir. Dördüncüsü, başladığından daha çirkin görünüyor.

[...] veya bu sadece kötü bir fikir mi?

Ya şununla kalırdım:

if (cake != null && cake.frosting != null && ...) ...

ya da Mehrdad Afshari'nin yukarıdaki cevabı ile devam edin.


Not: Bu cevabı yazdığımda, lambda fonksiyonları için ifade ağaçlarını açıkça düşünmemiştim; Bu yöndeki bir çözüm için örneğin @driis'in cevabına bakınız. Aynı zamanda bir tür düşünceye dayalıdır ve bu nedenle daha basit bir çözüm ( if (… != null & … != null) …) kadar iyi performans göstermeyebilir , ancak sözdizimi açısından daha iyi değerlendirilebilir.


2
Bunun neden reddedildiğini bilmiyorum, denge için bir oy verdim: Cevap doğrudur ve yeni bir bakış açısı getiriyor (ve bu çözümün dezavantajlarından açıkça bahsediyor ...)
MartinStettner

"Mehrdad Afshari'nin yukarıdaki cevabı" nerede?
Marson Mao

1
@MarsonMao: Bu cevap arada silinmiştir. (SO dereceniz yeterince yüksekse, yine de okuyabilirsiniz.) Hatamı belirttiğiniz için teşekkürler: "Yukarıya bakın" / "aşağıya bakın" gibi sözcükler kullanmadan, bir köprü kullanarak diğer yanıtlara başvurmalıyım (çünkü yanıtlar sabit bir sırada görünmez). Cevabımı güncelledim.
stakx -

5

Driis'in cevabı ilginç olsa da, performans açısından biraz fazla pahalı olduğunu düşünüyorum. Birçok temsilci derlemek yerine, özellik yolu başına bir lambda derlemeyi, onu önbelleğe almayı ve sonra birçok türü yeniden çağırmayı tercih ederim.

Aşağıdaki NullCoalesce tam da bunu yapar, herhangi bir yolun boş olması durumunda boş kontroller ve bir varsayılan dönüş (TResult) ile yeni bir lambda ifadesi döndürür.

Misal:

NullCoalesce((Process p) => p.StartInfo.FileName)

Bir ifade döndürecektir

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));

Kod:

    static void Main(string[] args)
    {
        var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked);
        var converted2 = NullCoalesce((string[] s) => s.Length);
    }

    private static Expression<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression)
    {
        var test = GetTest(lambdaExpression.Body);
        if (test != null)
        {
            return Expression.Lambda<Func<TSource, TResult>>(
                Expression.Condition(
                    test,
                    lambdaExpression.Body,
                    Expression.Default(
                        typeof(TResult)
                    )
                ),
                lambdaExpression.Parameters
            );
        }
        return lambdaExpression;
    }

    private static Expression GetTest(Expression expression)
    {
        Expression container;
        switch (expression.NodeType)
        {
            case ExpressionType.ArrayLength:
                container = ((UnaryExpression)expression).Operand;
                break;
            case ExpressionType.MemberAccess:
                if ((container = ((MemberExpression)expression).Expression) == null)
                {
                    return null;
                }
                break;
            default:
                return null;
        }
        var baseTest = GetTest(container);
        if (!container.Type.IsValueType)
        {
            var containerNotNull = Expression.NotEqual(
                container,
                Expression.Default(
                    container.Type
                )
            );
            return (baseTest == null ?
                containerNotNull :
                Expression.AndAlso(
                    baseTest,
                    containerNotNull
                )
            );
        }
        return baseTest;
    }


3

Ben de sık sık daha basit bir sözdizimi diledim! Eğer null olabilir yöntemi dönüşümsüz değerlerine sahip olduğunda o zaman ekstra değişkenleri gerekir, çünkü, özellikle çirkin alır (örneğin: cake.frosting.flavors.FirstOrDefault().loader)

Ancak, işte kullandığım oldukça iyi bir alternatif: bir Null-Safe-Chain yardımcı yöntemi oluşturun. Bunun @ John'un yukarıdaki cevabına oldukça benzediğinin farkındayım ( Coaluzatma yöntemiyle) ancak daha basit ve daha az yazarak buluyorum. Göründüğü gibi:

var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader);

İşte uygulama:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> r) 
where TA:class where TB:class where TC:class {
    if (a == null) return default(TResult);
    var B = b(a);
    if (B == null) return default(TResult);
    var C = c(B);
    if (C == null) return default(TResult);
    return r(C);
}

Ayrıca, zincirin bir değer türü veya varsayılanla bitmesine izin veren aşırı yüklerin yanı sıra birkaç aşırı yük (2 ila 6 parametreli) oluşturdum. Bu benim için gerçekten işe yarıyor!



1

İçerisinde ileri sürüldüğü gibi , John Leidegren 'ın cevabı , bir yaklaşım çalışması-etrafında bu uzantı yöntem ve delegeleri kullanmaktır. Bunları kullanmak şunun gibi görünebilir:

int? numberOfBerries = cake
    .NullOr(c => c.Frosting)
    .NullOr(f => f.Berries)
    .NullOr(b => b.Count());

Uygulama karmaşıktır çünkü değer türleri, başvuru türleri ve null yapılabilir değer türleri için çalışmasını sağlamanız gerekir. Sen tam bir uygulama bulabilirsiniz Timwi 'ın cevabı için null değerleri denetlemek için doğru yolu nedir? .


1

Ya da yansıma kullanabilirsiniz :)

Yansıma işlevi:

public Object GetPropValue(String name, Object obj)
    {
        foreach (String part in name.Split('.'))
        {
            if (obj == null) { return null; }

            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }

            obj = info.GetValue(obj, null);
        }
        return obj;
    }

Kullanımı:

object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj);

My Case (yansıma işlevinde null yerine DBNull.Value döndür):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));

1

Bu kodu deneyin:

    /// <summary>
    /// check deep property
    /// </summary>
    /// <param name="obj">instance</param>
    /// <param name="property">deep property not include instance name example "A.B.C.D.E"</param>
    /// <returns>if null return true else return false</returns>
    public static bool IsNull(this object obj, string property)
    {
        if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty");
        if (obj != null)
        {
            string[] deep = property.Split('.');
            object instance = obj;
            Type objType = instance.GetType();
            PropertyInfo propertyInfo;
            foreach (string p in deep)
            {
                propertyInfo = objType.GetProperty(p);
                if (propertyInfo == null) throw new Exception("No property : " + p);
                instance = propertyInfo.GetValue(instance, null);
                if (instance != null)
                    objType = instance.GetType();
                else
                    return true;
            }
            return false;
        }
        else
            return true;
    }

0

Bunu dün gece gönderdim ve sonra bir arkadaşım bana bu soruya işaret etti. Umarım yardımcı olur. Daha sonra şuna benzer bir şey yapabilirsiniz:

var color = Dis.OrDat<string>(() => cake.frosting.berries.color, "blue");


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace DeepNullCoalescence
{
  public static class Dis
  {
    public static T OrDat<T>(Expression<Func><T>> expr, T dat)
    {
      try
      {
        var func = expr.Compile();
        var result = func.Invoke();
        return result ?? dat; //now we can coalesce
      }
      catch (NullReferenceException)
      {
        return dat;
      }
    }
  }
}

Blog yazısının tamamını buradan okuyun .

Aynı arkadaş da bunu izlemeni önerdi .


3
ExpressionSadece derleyip yakalayacaksanız neden bir ile uğraşasınız ki? Sadece bir Func<T>.
Scott Rippey

0

Sorulan soru için çalışmasını sağlamak için kodu buradan biraz değiştirdim :

public static class GetValueOrDefaultExtension
{
    public static TResult GetValueOrDefault<TSource, TResult>(this TSource source, Func<TSource, TResult> selector)
    {
        try { return selector(source); }
        catch { return default(TResult); }
    }
}

Ve evet, bu muhtemelen dene / yakala performans etkileri nedeniyle en uygun çözüm değil ama işe yarıyor:>

Kullanımı:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);

0

Bunu başarmanız gereken yerde şunu yapın:

kullanım

Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color);

veya

Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color);

Yardımcı sınıf uygulaması

public static class Complex
{
    public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func)
    {
        return Get(() => func(root));
    }

    public static T Get<T>(Func<T> func)
    {
        try
        {
            return func();
        }
        catch (Exception)
        {
            return default(T);
        }
    }
}

-3

Objective-C tarafından alınan yaklaşımı seviyorum:

"Objective-C dili bu soruna başka bir yaklaşım getiriyor ve nil üzerinde yöntemleri çağırmıyor, bunun yerine tüm bu tür çağrılar için nil döndürüyor."

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}

1
başka bir dilin yaptığı şey (ve onun hakkındaki fikriniz) C # ile çalışmasını sağlamakla neredeyse tamamen alakasızdır. Kimsenin C # problemini çözmesine yardımcı olmaz
ADyson
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.