آیا تا به حال مجبور به نوشتن کدی شبیه قطعه کد زیر شده اید؟
var store = GetStore(); string postCode = null; if (store != null && store.Address != null && store.Address.PostCode != null) postCode = store.Address.PostCode.ToString();
بله! من مطمئن هستم برای شما هم پیش آمده است.
هدف بازیابی و یا محاسبه یک مقدار است، اما برای انجام این کار میبایست به چندین شیء میانی دسترسی پیدا کنیم که البته ممکن است در حالت پیش فرض خود قرار داشته باشند و حاوی هیچ مقداری نباشند. بنابراین برای جلوگیری از وقوع NullException ، مجبوریم تمامی اشیائی که در مسیر قرار دارند را بررسی کنیم که null نباشند. مثال بالا کاملا گویا ست. گاهی اوقات حتی ممکن است فراخوانی یک متد، تبدیل نوع با استفاده از as و یا دسترسی به عناصر یک مجموعه وجود داشته باشد. متاسفانه مدیریت تمامی این حالات باعث حجیم شدن کدها و در نتیجه کاهش خوانایی آنها میشود. بنابراین باید به دنبال یک راه حل مناسب بود.
استفاده از یک متد الحاقی شرطی (Conditional extensions)
از نظر بسیاری از برنامه نویسها راه حل، استفاده از یک متد الحاقی شرطی است. اگر عبارت "c# deep null check" را گوگل کنید، پیاده سازیهای متنوعی را پیدا خواهید کرد. اگر چه این متدها نامهای متفاوتی دارند اما همه آنها از یک ایده کلی مشترک استفاده میکنند:
public static TResult IfNotNull<TResult, TSource>( this TSource source, Func<TSource, TResult> onNotDefault) where TSource : class { if (onNotDefault == null) throw new ArgumentNullException("onNotDefault"); return source == null ? default(TResult) : onNotDefault(source); }
همانطور که میبینید این متد الحاقی مقداری از نوع TResult را بر میگرداند. اگر source که در اینجا با توجه به الحاقی بودن متد به معنای شی جاری است، null باشد مقدار پیش فرض نوع خروجی(TResult) بازگردانده میشود و در غیر این صورت دیلیگیت onNotDefault فراخوانی میگردد.
بعد از افزودن متد الحاقی IfNotNull به پروژه میتوانیم مثال ابتدای مطلب را به صورت زیر بنویسیم :
var postCode = GetStore() .IfNotNull(x => x.Address) .IfNotNull(x => x.PostCode) .IfNotNull(x => x.ToString());
این روش مزایای بسیاری دارد اما در موارد پیچیده دچار مشکل میشویم. برای مثال در نظر بگیرید قصد داریم در طول مسیر، متدی را فراخوانی کنیم و مقدار بازگشتی را در یک متغیر موقتی ذخیره کنیم و بر اساس آن ادامه مسیر را طی کنیم. متاسفانه این کارها هم اکنون امکان پذیر نیست. پس به نظر میرسد باید کمی متد الحاقی IfNotNull را بهبود ببخشیم.
برای بهبود عملکرد متد الحاقی IfNotNull علاوه بر موارد ذکر شده حداقل دو مورد به نظر من میرسد:
- این متد فقط با انواع ارجاعی (reference types) کار میکند و میبایست برای کار با انواع مقداری (value types) اصلاح شود.
- با انواع داده ای مثل string چه باید کرد؟ در مورد این نوع دادهها تنها مطمئن شدن از null نبودن کافی نیست. برای مثال در مورد string ، گاهی اوقات ما میخواهیم از خالی نبودن آن نیز مطمئن شویم. و یا در مورد collectionها تنها null نبودن کافی نیست بلکه زمانی که نیاز به محاسبه مجموع و یا یافتن بزرگترین عضو است، باید از خالی نبودن مجموعه و وجود حداقل یک عضو در آن مطمئن باشیم.
برای حل این مشکلات میتوانیم متد الحاقی IfNotNull را به متد الحاقی IfNotDefault تبدیل کنیم:
public static TResult IfNotDefault<TResult, TSource>( this TSource source, Func<TSource, TResult> onNotDefault, Predicate<TSource> isNotDefault = null) { if (onNotDefault == null) throw new ArgumentNullException("onNotDefault"); var isDefault = isNotDefault == null ? EqualityComparer<TSource>.Default.Equals(source, default(TSource)) : !isNotDefault(source); return isDefault ? default(TResult) : onNotDefault(source); }
تعریف این متد خیلی با تعریف متد قبلی متفاوت نیست. به منظور پشتیبانی از struct ها، قید where TSource : class حذف شده است. بنابراین دیگر نمیتوان از مقایسهی ساده null با استفاده از عملگر == استفاده کرد چراکه structها nullable نیستند. پس مجبوریم از EqualityComparer<TSource>.Default بخواهیم که این کار را انجام دهد. متد الحاقی IfNotDefault همچنین شامل یک predicate اختیاری با نام isNotDefault است. در صورتی که مقایسه پیش فرض کافی نبود میتوان از این predicate استفاده کرد.
در انتها اجازه بدهید چند مثال کاربردی را مرور کنیم:
1- انجام یک سری اعمال بر روی string در صورتی که رشته خالی نباشد:
return person . IfNotDefault(x => x.Name) . IfNotDefault(SomeOperation, x => !string.IsNullOrEmpty(x));
محاسبهی مقدار میانگین. متد الحاقی IfNotDefault به زیبایی در یک زنجیرهی LINQ کار میکند:
var avg = students .Where(IsNotAGraduate) .FirstOrDefault() .IfNotDefault(s => s.Grades) .IfNotDefault(g => g.Average(), g => g != null && g.Length > 0);
برای مطالعه بیشتر
Get rid of deep null checks
Chained null checks and the Maybe monad
Maybe or IfNotNull using lambdas for deep expressions
Dynamically Check Nested Values for IsNull Values