Deep null checking, apakah ada cara yang lebih baik?


130

Catatan: Pertanyaan ini ditanyakan sebelum pengenalan yang .?operator dalam C # 6 / Visual Studio 2015 .

Kita semua pernah ke sana, kita memiliki beberapa properti dalam seperti cake.frosting.berries.loader yang perlu kita periksa apakah itu nol sehingga tidak ada pengecualian. Cara yang harus dilakukan adalah menggunakan pernyataan hubungan arus pendek

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

Ini tidak sepenuhnya elegan, dan mungkin harus ada cara yang lebih mudah untuk memeriksa seluruh rantai dan melihat apakah itu muncul terhadap variabel null / properti.

Apakah mungkin menggunakan beberapa metode ekstensi atau apakah itu fitur bahasa, atau itu hanya ide yang buruk?


3
Saya cukup berharap untuk itu - tetapi semua ide yang saya kemukakan lebih buruk daripada masalah yang sebenarnya.
peterchen

Terima kasih atas semua jawaban dan menarik untuk melihat bahwa orang lain memiliki pemikiran yang sama. Saya harus memikirkan bagaimana saya ingin melihat ini diselesaikan sendiri dan meskipun solusi Eric bagus, saya pikir saya hanya akan menulis sesuatu seperti ini jika (IsNull (abc)), atau jika (IsNotNull (abc)) tapi mungkin itu hanya untuk seleraku :)
Homde

Ketika Anda instantiate frosting, ia memiliki sifat buah beri, jadi pada titik itu di konstruktor Anda, dapatkah Anda memberi tahu frosting bahwa setiap kali digunakan untuk membuat buah beri kosong (bukan-nol)? dan kapan pun buah beri dimodifikasi, apakah pengecekan nilainya ????
Doug Chamberlain

Agak longgar terkait, beberapa teknik di sini saya menemukan lebih disukai untuk masalah "nol dalam" yang saya coba untuk berkeliling. stackoverflow.com/questions/818642/…
AaronLS

Jawaban:


223

Kami telah mempertimbangkan untuk menambah operasi baru "?." ke bahasa yang memiliki semantik yang Anda inginkan. (Dan itu telah ditambahkan sekarang; lihat di bawah.) Yaitu, Anda akan mengatakan

cake?.frosting?.berries?.loader

dan kompiler akan menghasilkan semua pemeriksaan arus pendek untuk Anda.

Itu tidak membuat bar untuk C # 4. Mungkin untuk versi bahasa hipotetis masa depan.

Update (2014): The ?.operator sekarang direncanakan untuk Roslyn rilis compiler berikutnya. Perhatikan bahwa masih ada beberapa perdebatan tentang analisis sintaksis dan semantik yang tepat dari operator.

Pembaruan (Juli 2015): Visual Studio 2015 telah dirilis dan dikirimkan dengan kompiler C # yang mendukung operator null-kondisional ?.dan?[] .


10
Tanpa titik itu menjadi ambigu secara sintaktis dengan operator kondisional (A? B: C). Kami mencoba menghindari konstruksi leksikal yang mengharuskan kami untuk "melihat ke depan" secara sewenang-wenang jauh di dalam token stream. (Meskipun, sayangnya, sudah ada konstruksi seperti itu di C #; kami lebih suka tidak menambahkan lagi.)
Eric Lippert

33
@Ian: masalah ini sangat umum. Ini adalah salah satu permintaan yang paling sering kita dapatkan.
Eric Lippert

7
@ Ian: Saya juga lebih suka menggunakan pola objek nol ketika layak untuk melakukannya, tetapi kebanyakan orang tidak memiliki kemewahan bekerja dengan model objek yang mereka desain sendiri. Banyak model objek yang ada menggunakan null dan itulah dunia yang harus kita tinggali.
Eric Lippert

12
@ John: Kami mendapatkan permintaan fitur ini hampir seluruhnya dari pemrogram kami yang paling berpengalaman. MVP meminta ini sepanjang waktu . Tetapi saya mengerti bahwa pendapat berbeda; jika Anda ingin memberikan saran desain bahasa yang konstruktif selain kritik Anda, saya senang untuk mempertimbangkannya.
Eric Lippert

28
@ lazyberezovsky: Saya tidak pernah mengerti apa yang disebut "hukum" Demeter; pertama-tama, tampaknya lebih tepatnya disebut "The Suggestion of Demeter". Dan kedua, hasil dari mengambil "hanya satu akses anggota" ke kesimpulan logisnya adalah "objek Tuhan" di mana setiap objek diminta untuk melakukan segalanya untuk setiap klien, daripada mampu membagikan benda yang tahu bagaimana melakukan apa yang klien inginkan. Saya lebih suka kebalikan dari hukum demeter: setiap objek memecahkan sejumlah kecil masalah dengan baik, dan salah satu solusi tersebut dapat "di sini objek lain yang memecahkan masalah Anda lebih baik"
Eric Lippert

27

Saya terinspirasi oleh pertanyaan ini untuk mencoba dan mencari tahu bagaimana pemeriksaan nol dalam seperti ini dapat dilakukan dengan sintaks yang lebih mudah / lebih cantik menggunakan pohon ekspresi. Sementara saya setuju dengan jawaban yang menyatakan bahwa itu mungkin desain yang buruk jika Anda sering perlu mengakses instance jauh di dalam hierarki, saya juga berpikir bahwa dalam beberapa kasus, seperti presentasi data, itu bisa sangat berguna.

Jadi saya membuat metode ekstensi, yang akan memungkinkan Anda untuk menulis:

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

Ini akan mengembalikan Berries jika tidak ada bagian dari ekspresi yang nol. Jika null ditemukan, null dikembalikan. Ada beberapa peringatan, dalam versi saat ini hanya akan bekerja dengan akses anggota yang sederhana, dan hanya bekerja pada .NET Framework 4, karena menggunakan metode MemberExpression.Update, yang baru di v4. Ini adalah kode untuk metode ekstensi IfNotNull:

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

Ini bekerja dengan memeriksa pohon ekspresi yang mewakili ekspresi Anda, dan mengevaluasi bagian-bagian satu demi satu; setiap kali memeriksa bahwa hasilnya tidak nol.

Saya yakin ini dapat diperluas sehingga ekspresi lain selain MemberExpression didukung. Anggap ini sebagai kode pembuktian konsep, dan harap diingat bahwa akan ada penalti performa dengan menggunakannya (yang mungkin tidak akan menjadi masalah dalam banyak kasus, tetapi jangan menggunakannya dalam lingkaran ketat :-))


Saya terkesan dengan keterampilan lambda Anda :) sintaksnya tampaknya sedikit lebih kompleks daripada yang diinginkan,
setidaknya untuk

Keren, tetapi berjalan seperti kode 100x lebih banyak daripada if .. &&. Ini hanya bermanfaat jika masih dikompilasi ke if .. &&.
Monstieur

1
Ah lalu saya lihat di DynamicInvokesana. Saya dengan religius menghindarinya :)
nawfal

24

Saya telah menemukan ekstensi ini sangat berguna untuk skenario bersarang yang mendalam.

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

Ini adalah ide yang saya dapatkan dari operator penggabungan nol dalam C # dan T-SQL. Yang menyenangkan adalah bahwa tipe pengembalian selalu menjadi tipe pengembalian properti dalam.

Dengan begitu Anda dapat melakukan ini:

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

... atau sedikit variasi di atas:

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

Itu bukan sintaks terbaik yang saya tahu, tetapi itu berhasil.


Kenapa "Batubara", itu terlihat sangat menyeramkan. ;) Namun, sampel Anda akan gagal jika pembekuan nol. Seharusnya tampak seperti: var berry = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke

Oh, tapi Anda menyiratkan bahwa argumen kedua bukanlah panggilan ke Coal yang tentu saja harus. Itu hanya perubahan yang nyaman. Selektor (x => x.berries) diteruskan ke panggilan Batubara di dalam metode Batubara yang mengambil dua argumen.
John Leidegren

Nama penggabungan atau penggabungan diambil dari T-SQL, di situlah saya pertama kali mendapat ide. IfNotNull menyiratkan bahwa sesuatu terjadi jika tidak nol, namun apa itu, tidak dijelaskan oleh pemanggilan metode IfNotNull. Batubara memang nama yang aneh, tetapi ini dalam akta metode yang aneh layak diperhatikan.
John Leidegren

Nama terbaik untuk ini adalah "ReturnIfNotNull" atau "ReturnOrDefault"
John Leidegren

@ flq +1 ... dalam proyek kami ini juga disebut IfNotNull :)
Marc Sigrist

16

Selain melanggar Hukum Demeter, seperti yang telah ditunjukkan oleh Mehrdad Afshari, menurut saya Anda perlu "pemeriksaan nol dalam" untuk logika keputusan.

Ini paling sering terjadi ketika Anda ingin mengganti objek kosong dengan nilai default. Dalam hal ini Anda harus mempertimbangkan menerapkan Pola Objek Null . Ini bertindak sebagai stand-in untuk objek nyata, memberikan nilai default dan metode "non-aksi".


tidak, objektif-c memungkinkan pengiriman pesan ke objek nol dan mengembalikan nilai default yang sesuai jika perlu. Tidak ada masalah di sana.
Johannes Rudolph

2
Ya. Itulah intinya. Pada dasarnya, Anda akan meniru perilaku ObjC dengan Pola Objek Null.
Mehrdad Afshari

10

Pembaruan: Dimulai dengan Visual Studio 2015, kompiler C # (versi bahasa 6) sekarang mengenali ?.operator, yang membuat "pemeriksaan nol dalam" menjadi mudah. Lihat jawaban ini untuk detailnya.

Selain mendesain ulang kode Anda, seperti yang disarankan oleh jawaban yang dihapus ini , pilihan lain (walaupun mengerikan) adalah menggunakan try…catchblok untuk melihat apakah NullReferenceExceptionterjadi suatu saat selama pencarian properti mendalam tersebut.

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

Saya pribadi tidak akan melakukan ini karena alasan berikut:

  • Itu tidak terlihat bagus.
  • Ini menggunakan penanganan pengecualian, yang harus menargetkan situasi luar biasa dan bukan sesuatu yang Anda harapkan akan sering terjadi selama operasi normal.
  • NullReferenceExceptionMungkin seharusnya tidak pernah ditangkap secara eksplisit. (Lihat pertanyaan ini .)

Jadi apakah mungkin menggunakan beberapa metode ekstensi atau apakah itu fitur bahasa, [...]

Ini hampir pasti harus menjadi fitur bahasa (yang tersedia dalam C # 6 dalam bentuk .?dan ?[]operator), kecuali C # sudah memiliki evaluasi malas yang lebih canggih, atau kecuali jika Anda ingin menggunakan refleksi (yang mungkin juga bukan ide yang bagus untuk alasan kinerja dan keamanan tipe).

Karena tidak ada cara untuk hanya beralih cake.frosting.berries.loaderke fungsi (itu akan dievaluasi dan melempar pengecualian referensi nol), Anda harus menerapkan metode pencarian umum dengan cara berikut: Dibutuhkan objek dan nama properti untuk menengadah:

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

(Catatan: kode diedit.)

Anda dengan cepat melihat beberapa masalah dengan pendekatan seperti itu. Pertama, Anda tidak mendapatkan keamanan tipe apa pun dan kemungkinan tinju nilai properti dari tipe sederhana. Kedua, Anda dapat kembali nulljika terjadi kesalahan, dan Anda harus memeriksa ini di fungsi panggilan Anda, atau Anda melempar pengecualian, dan Anda kembali ke tempat Anda memulai. Ketiga, mungkin lambat. Keempat, terlihat lebih buruk dari apa yang Anda mulai.

[...], atau itu hanya ide yang buruk?

Saya akan tetap dengan:

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

atau pergi dengan jawaban di atas oleh Mehrdad Afshari.


PS: Dulu ketika saya menulis jawaban ini, saya jelas tidak mempertimbangkan pohon ekspresi untuk fungsi lambda; lihat misalnya jawaban @driis untuk solusi ke arah ini. Ini juga didasarkan pada semacam refleksi dan karenanya mungkin tidak berkinerja sebaik solusi yang lebih sederhana ( if (… != null & … != null) …), tetapi mungkin dinilai lebih baik dari sudut pandang sintaksis.


2
Saya tidak tahu mengapa ini diturunkan, saya melakukan upvote untuk keseimbangan: Jawabannya benar dan membawa aspek baru (dan secara jelas menyebutkan kelemahan dari solusi ini ...)
MartinStettner

di mana "jawaban di atas oleh Mehrdad Afshari"?
Marson Mao

1
@MarsonMao: Jawaban itu telah dihapus sementara itu. (Anda masih dapat membacanya jika peringkat SO Anda cukup tinggi.) Terima kasih telah menunjukkan kesalahan saya: Saya harus merujuk ke jawaban lain menggunakan hyperlink, tidak menggunakan kata-kata seperti "lihat di atas" / "lihat di bawah" (karena jawaban tidak muncul dalam urutan tetap). Saya telah memperbarui jawaban saya.
stakx - tidak lagi berkontribusi

5

Sementara jawaban driis menarik, saya pikir itu kinerja yang terlalu mahal. Daripada mengkompilasi banyak delegasi, saya lebih suka mengkompilasi satu lambda per path properti, cache dan kemudian retvoke banyak jenis.

NullCoalesce di bawah tidak hanya itu, ia mengembalikan ekspresi lambda baru dengan cek nol dan pengembalian default (TResult) jika ada jalur yang null.

Contoh:

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

Akan mengembalikan ekspresi

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

Kode:

    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

Saya juga sering berharap untuk sintaksis yang lebih sederhana! Itu menjadi sangat jelek ketika Anda memiliki nilai metode-pengembalian-yang mungkin nol, karena Anda perlu variabel tambahan (misalnya:cake.frosting.flavors.FirstOrDefault().loader :)

Namun, inilah alternatif yang lumayan yang saya gunakan: buat metode pembantu Null-Safe-Chain. Saya menyadari bahwa ini sangat mirip dengan jawaban John di atas (dengan Coalmetode ekstensi) tetapi saya merasa lebih mudah dan kurang mengetik. Begini tampilannya:

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

Inilah implementasinya:

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

Saya juga membuat beberapa overload (dengan 2 hingga 6 parameter), serta overload yang memungkinkan rantai diakhiri dengan tipe-nilai atau default. Ini bekerja sangat baik untuk saya!



1

Seperti yang disarankan di John Leidegren 's jawaban , salah satu pendekatan untuk kerja-sekitar ini adalah dengan menggunakan metode penyuluhan dan delegasi. Menggunakannya bisa terlihat seperti ini:

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

Implementasinya berantakan karena Anda harus membuatnya bekerja untuk tipe nilai, tipe referensi, dan tipe nilai nullable. Anda dapat menemukan implementasi lengkap di Timwi 's jawaban untuk Apa cara yang tepat untuk memeriksa nilai null? .


1

Atau Anda dapat menggunakan refleksi :)

Fungsi refleksi:

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

Pemakaian:

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

Kasus Saya (mengembalikan DBNull.Value alih-alih nol dalam fungsi refleksi):

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

1

Coba kode ini:

    /// <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

Saya memposting ini tadi malam dan kemudian seorang teman menunjuk saya ke pertanyaan ini. Semoga ini bisa membantu. Anda kemudian dapat melakukan sesuatu seperti ini:

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

Baca posting blog selengkapnya di sini .

Teman yang sama juga menyarankan agar Anda menonton ini .


3
Mengapa repot-repot dengan Expressionjika Anda hanya akan mengkompilasi dan menangkap? Cukup gunakan a Func<T>.
Scott Rippey

0

Saya sedikit memodifikasi kode dari sini untuk membuatnya berfungsi untuk pertanyaan yang diajukan:

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

Dan ya, ini mungkin bukan solusi optimal karena mencoba / menangkap implikasi kinerja tetapi berfungsi:>

Pemakaian:

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

0

Di mana Anda perlu mencapai ini, lakukan ini:

Pemakaian

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

atau

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

Implementasi kelas pembantu

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

Saya suka pendekatan yang diambil oleh Objective-C:

"Bahasa Objective-C mengambil pendekatan lain untuk masalah ini dan tidak memanggil metode pada nol tetapi sebaliknya mengembalikan nol untuk semua doa seperti itu."

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

1
apa yang dilakukan oleh bahasa lain (dan pendapat Anda tentang hal itu) hampir sepenuhnya tidak relevan untuk membuatnya berfungsi dalam C #. Itu tidak membantu siapa pun untuk memecahkan masalah C # mereka
ADyson
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.