پیاده سازی Option یا Maybe در #C
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

Options یا Maybe در یک زبان تابعی مثل #F، نشان دهنده‌ی این است که شیء (Object) ممکن است وجود نداشته باشد(Null Reference) که یکی از مهمترین ویژگی‌های یک زبان شیءگرا مثل #C و یا Java محسوب می‌شودما برنامه نویس‌ها (اغلب) از هرچیزی که باعث کرش برنامه می‌شود، بیزاریم و برای اینکه برنامه کرش نکند، مجبور میشویم تمام کد‌های خود  را از Null Reference محافظت کنیم. تمام این مشکلات توسط Tony Hoare مخترع ALOGL است که تنها دلیل وجود Null References را سادگی پیاده سازی آن می‌داند و او این مورد را یک «خطای  میلیون دلاری» نامیده‌است. 

به این مثال توجه بفرمایید: 

public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

public class UserService : IUserService
    {
        private IList<User> _userData;

        public UserService()
        {
            _userData = new List<User>
            {
                new User {Id = 1,Name = "ali"},
                new User {Id = 2,Name = "Karim"}
            };
        }

        public User GetById(int id)
        {
            return _userData.FirstOrDefault(x => x.Id == id);
        }
    }  

public class UserController : Controller
    {
        private readonly IUserService _userService;

        public UserController(IUserService  userService)
        {
            _userService = userService;
        }
        public ActionResult Details(int id)
        {
            var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند                           
            if( user == null)
                 return HttpNotFound();    
            return View(user);  
        }
    }

این کدی است که ما برنامه نویسان به صورت متداولی با آن سروکار داریم. اما چه چیزی درباره این کد اشکال دارد؟

مشکل از آن جایی هست که ما نمی‌دانیم متد GetById مقداری را برمیگرداند و یا Null را بر می‌گرداند. این متد هرگاه که امکان برگرداندن Null وجود داشته باشد، خطای  NullReferenceException را در زمان اجرا بر می‌گرداند و همان طور که میدانید، به ازای هر شرطی که به برنامه اضافه میکنیم، پیچیدگی برنامه هم افزایش می‌یابد و کد خوانایی خود را از دست می‌دهد. تصور کنید دنیایی بدون NullReferenceException چه دنیایی زیبایی می‌بود؛ ولی متاسفانه این مورد از ویژگی‌های زبان #C است. خوشبختانه راه‌حل‌های برای حل NRE ارائه شده‌اند که در ادامه به آن‌ها می‌پردازیم.

ما می‌خواهیم متد GetById همیشه چیزی غیر از نال را برگرداند و یکی از راه‌هایی که ما را به این هدف می‌رساند این است که این متد یک توالی را برگرداند.

به نگاری جدید کد توجه بفرمایید:
public class UserService : IUserService
    {
        private IList<User> _userData;

        public UserService()
        {
            _userData = new List<User>
            {
                new User {Id = 1,Name = "ali"},
                new User {Id = 2,Name = "Karim"}
            };
        }

        public IEnumerable<User> GetById(int id)
        {
            var user = _userData.FirstOrDefault(x => x.Id == id);
            if (user == null) return new User[0];
            return new[] { user };
        }
    } 

اگر به امضای متد GetById توجه کنید، به جای اینکه User را برگرداند، این متد یک توالی از User را بر می‌گرداند و اگر در اینجا کاربری یافت شد، این توالی دارای یک المان خواهد بود و در غیر این صورت اگر User یافت نشد، این متد یک توالی را بر می‌گرداند که دارای هیچ المانی نیست. در ادامه اگر کلاینت بخواهد از متد GetById استفاده کند، به صورت زیر خواهد بود:

 public ActionResult Details(int id)
        {
            var user = _userService
                            .GetById(3)
                            .DefaultIfEmpty(new User())
                            .Single();
            return View(user);
        }

 متد GetById دارای دو وجه است و وجه مثبت آن این است که اگر مجموعه دارای مقداری باشد، هیچ مشکلی نیست؛ ولی اگر مجموعه دارای المانی نباشد، باید یک شیء را به صورت پیش فرض به آن اختصاص دهیم که این کار را با استفاده از متد DefualtIfEmpty انجام داده‌ایم. 

 در اول مقاله هم اشاره کردیم که  Maybe یا Options، مجموعه‌ای است که دارای یک المان و یا هیچ المانی است. اگر به امضای متد GetById توجه کنید، متوجه خواهید شد که این متد می‌تواند مجموعه‌ای را برگرداند و نمی‌تواند گارانتی کند که حتما مجموعه‌ای را بر می‌گرداند که دارای یک المان و یا هیچ باشد. برای حل این مشکل می‌توانیم از کلاس Option استفاده کنیم:

public class Option<T> : IEnumerable<T>
    {
        private readonly T[] _data;

        private Option(T[] data)
        {
            _data = data;
        }

        public static Option<T> Create(T element) => new Option<T>(new[] { element });

        public static Option<T> CreateEmpty() => new Option<T>(new T[0]);

        public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) _data).GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
    }

تنها دلیل استفاده از متد‌های Create و CreateEmpty این است که به خوانایی برنامه کمک کنیم؛ نه بیشتر. در ادامه اگر بخواهیم از کلاس option استفاده کنیم، به صورت زیر خواهد بود:

 public class UserService : IUserService
    {
       ...
       ...
       public Option<User> GetById(int id)
        {
            var user = _userData.FirstOrDefault(x => x.Id == id);
            return user == null ? Option<User>.CreateEmpty() : Option<User>.Create(user);
        }
    }

 public class UserController : Controller
    {
       ...
       ...
       public ActionResult Details(int id)
        {
            var user = _userService
                            .GetById(3)
                            .DefaultIfEmpty(new User())
                            .Single();
            return View(user);
        }
    }


چکیده:

مدیریت کردن References کار بسیار پیچیده‌ای است. قبل از آن که تلاش کنیم مقداری را برگردانیم و یا عملیاتی را بر روی آن انجام دهیم، اول باید مطمئن شویم که این شیء به جایی اشاره می‌کند. نمونه‌های متفاوتی از Option و یا Maybe را می‌توانید در اینترنت پیدا کنید که هدف نهایی آن‌ها، حذف NullReferenceException است و آشنایی با این ایده، شما را به دنیای برنامه نویسی تابعی در#C هدایت می‌کند.

  • #
    ‫۷ سال و ۹ ماه قبل، شنبه ۱۳ آذر ۱۳۹۵، ساعت ۱۶:۲۷
    در اولین کد نوشته اید:
     اگر user==null باشد خطای HttpNotFound و در غیراینصورت ویوی متناظر برگشت داده شود.
    در ادامه "نوشتن if  اضافه جهت چک کردن نال رفرنس" را بعنوان ایراد مطرح کردید.
    اما در دومین کد، رفتار متد تغییر کرد تا if حذف شود (در هر صورت یک ویو برگشت داده می‌شود).
    بجای
            public ActionResult Details(int id)
            {
                var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند
                if( user == null)
                     return HttpNotFound();
                return View(user);
            }
    نوشتید:
    public ActionResult Details(int id)
            {
                var user = _userService
                                .GetById(3)
                                .DefaultIfEmpty(new User())
                                .Single();
                return View(user);
            }

    اگر هدف متد؛ انجام کار به شکل کد دوم باشد میتوان همان متد اولیه را بدون استفاده از Maybe و بصورت تک خطی نوشت:
    public ActionResult Details(int id)
    {
        return View(_userService.GetById(3) ?? new User()); 
    }
    هم کدها ساده شده اند و هم if و خیلی چیزهای دیگه حذف شده اند.
    بهرحال در نسخه نهایی بررسی نال رفرنس همچنان وجود دارد و تنها خودش رو به شکل دیگری بروز میدهد: DefaultIfEmpty(new User()).
    آیا با این روش فقط کدها را پیچیده‌تر نمیکنیم؟ (نقض KISS )
    آیا میشود همان منطق اولیه را با این روش انجام داد؟ (بدون پاسکاری یا موکول کردن آن به متد دیگر)
    public ActionResult Details(int id)
    {
        var user = _userService.GetById(3);
        return user == null ? HttpNotFound() : View(user);
    }


    آیا در کل لزومی به استفاده از Maybe یا Option هست؟ 
    ...
    • #
      ‫۷ سال و ۹ ماه قبل، شنبه ۱۳ آذر ۱۳۹۵، ساعت ۱۸:۲۱
      آیا در کل لزومی به استفاده از Maybe یا Option هست؟  بله
      همانطور که در آخر این  مقاله اشاره کردم آشنایی با ایده Option شما رو به سمت برنامه نویسی تابعی می‌کشاند . برنامه نویسی تابعی به دلیل دارا بودن خاصیت  Immutable   ،  دارای مزایایی   هست که نمیشود به راحتی از آن عبور کرد.برای مثال اگر کلاینت ( مصرف کننده )  به امضای متد FindByIdAsync نگاه کند تنها برداشتی که میتواند کند این هست که این متد " ممکن است پستی رو برگرداند"، همین ویژگی خوانایی برنامه رو بالا میبرد و دیگر نیاز نیست که به صورت کامنت بالای کد خودتون بنویسید "این کد ممکن است که خطای NRE و بلا بلا بلا..." صادر کند بلکه امضای متد همچین کاری برای شما نجام میدهد و دیگر نیازی به کامنت نیست.
       برنامه نویسی تابعی باعث می‌شود که کد قابل نگهداری ، خوانایی و غیره رو افرایش دهد.به این مثال توجه کنید:
       public async Task<Maybe<Post>> FindByIdAsync(Guid id)
              {
                  Maybe<Post> post = await _posts.FindAsync(id);
                  return post;
              }
      و اگر بخواهیم از متد FindById در کد دیگر استفاده کنیم به صورت زیر خواهد بود :
       public async Task<Result> DeletePost(Guid id)
              {
                  return await FindByIdAsync(id)
                      .ToResult("پست مورد نظر یافت نشد.")
                      .OnSuccess(x => _posts.Remove(x))
                      .OnSuccess(x => _unitOfWork.SaveChangesAsync())
                      .OnBoth(x => x.IsSuccess ? Result.Ok() : Result.Fail(x.Error));
              }

      • #
        ‫۷ سال و ۹ ماه قبل، شنبه ۱۳ آذر ۱۳۹۵، ساعت ۱۹:۵۹
         مزیتهای برنامه نویسی تابعی برکسی پوشیده نیست، حتی زبان شی گرای سی شارپ^ ، پشتیبانی نسبتا خوبی از مفاهیم برنامه نویسی فانکشنال ارائه میدهد.(LINQ)
        اما در مورد مثال ارائه شده کاربرد چشمگیری از مفاهیم برنامه نویسی تابعی عنوان نشد. در واقع یک الگو زمانی کارساز است که همسو با کاربرد مرتبط (نیل به هدف) مورد استفاده قرار گیرد تا هرچه بیشتر مثمر ثمر واقع شود.
          • #
            ‫۷ سال و ۹ ماه قبل، یکشنبه ۱۴ آذر ۱۳۹۵، ساعت ۱۹:۰۵
            منظور من رو متوجه نشدید. بذارید با کد توضیح بدم:
            میخواهیم طبق هدف مقاله، این تکه کد را اصلاح کنیم.
            public ActionResult Details(int id)
                    {
                        var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند
                        if( user == null)
                             return HttpNotFound();
                        return View(user);
                    }
            راهکار ارائه شده:
            public ActionResult Details(int id)
                    {
                        var user = _userService
                                        .GetById(3)
                                        .DefaultIfEmpty(new User())
                                        .Single();
                        return View(user);
                    }
            پر واضح است خروجی این دو متد با هم یکسان نیستند.
            راه حل ارائه شده کامل نیست و با تغییر صورت مساله، به جواب دیگری میرسد.
            باید به کدی مثل این برسیم:
            public ActionResult Details(int id)
            {
                return 
                    Search<ActionResult>(id)
                    .OnExistValue(View("Details"))
                    .OnNotExistValue(new HttpNotFoundResult())
                    .ToValue();
            }
            برای نیل به این هدف:
            public class Maybe<T, TResult> : IEnumerable<T>
                {
                    private readonly T[] _data;
                    private readonly TResult _result;
            
                    private Maybe(T[] data)
                    {
                        _data = data;
                    }
            
                    private Maybe(TResult result)
                    {
                        _result = result;
                    }
            
                    public TResult ToValue() => _result;
                    public Maybe<T, TResult> OnExistValue(TResult result) => _data.Any() ? new Maybe<T, TResult>(result) : this;
            
                    public Maybe<T, TResult> OnNotExistValue(TResult result) => _result == null ? new Maybe<T, TResult>(result) : this; 
            
                    public static Maybe<T, TResult> Create(T element) => new Maybe<T, TResult>(new[] {element});
            
                public static Maybe<T, TResult> CreateEmpty() => new Maybe<T, TResult>(new T[0]);
            
                public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) _data).GetEnumerator();
            
                IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
            }
            و متد جستجو نیز ایچنین تغییر خواهد کرد:
            public Maybe<User, TResult> Search<TResult>(int id)
            {
                var lst = new User[] {};
            
                var r = lst.Where(x => x.Id == id).ToList();
                return r.Any() ? Maybe<User, TResult>.Create(r[0]) : Maybe<User, TResult>.CreateEmpty();
            }
            یکی از راه حل‌ها میتواند این کدها باشند.
          • #
            ‫۷ سال و ۹ ماه قبل، یکشنبه ۱۴ آذر ۱۳۹۵، ساعت ۱۹:۳۹
            درسته.
            ولی آیا کدها ساده‌تر نشده اند؟ یا جواب ناصحیح برگشت میدهند؟
  • #
    ‫۷ سال و ۹ ماه قبل، شنبه ۱۳ آذر ۱۳۹۵، ساعت ۲۰:۴۰
    بحثی هم در مورد non-nullable reference types  و اضافه شدن آن به #C در حال بررسی است.
  • #
    ‫۶ سال و ۱ ماه قبل، چهارشنبه ۳ مرداد ۱۳۹۷، ساعت ۲۲:۱۰
    با تشکر از شما
    لزوما با پیاده سازی ارائه شده در مطلب جاری، از شر بررسی Null بودن یا نبودن خلاص نشده ایم (از دید استفاده کننده) چرا که خروجی متد همچنان می‌تواند Nullable باشد (کلاس Option یک نوع ارجاعی می‌باشد). چرا که استفاده کننده از آن لازم است برروی خروجی خود متد که یک وهله از Option می‌باشد بررسی Null بودن یا عدم آن را انجام دهد. برای رهایی از این موضوع استفاده از struct راه حل معقولی می‌باشد؛ یک پیاده سازی از آن به صورت زیر می‌باشد:
        public struct Maybe<T> : IEquatable<Maybe<T>>
            where T : class
        {
            private readonly T _value;
    
            private Maybe(T value)
            {
                _value = value;
            }
    
            public bool HasValue => _value != null;
            public T Value => _value ?? throw new InvalidOperationException();
            public static Maybe<T> None => new Maybe<T>();
    
    
            public static implicit operator Maybe<T>(T value)
            {
                return new Maybe<T>(value);
            }
    
            public static bool operator ==(Maybe<T> maybe, T value)
            {
                return maybe.HasValue && maybe.Value.Equals(value);
            }
    
            public static bool operator !=(Maybe<T> maybe, T value)
            {
                return !(maybe == value);
            }
    
            public static bool operator ==(Maybe<T> left, Maybe<T> right)
            {
                return left.Equals(right);
            }
    
            public static bool operator !=(Maybe<T> left, Maybe<T> right)
            {
                return !(left == right);
            }
    
            /// <inheritdoc />
            /// <summary>
            ///     Avoid boxing and Give type safety
            /// </summary>
            /// <param name="other"></param>
            /// <returns></returns>
            public bool Equals(Maybe<T> other)
            {
                if (!HasValue && !other.HasValue)
                    return true;
    
                if (!HasValue || !other.HasValue)
                    return false;
    
                return _value.Equals(other.Value);
            }
    
            /// <summary>
            ///     Avoid reflection
            /// </summary>
            /// <param name="obj"></param>
            /// <returns></returns>
            public override bool Equals(object obj)
            {
                if (obj is T typed)
                {
                    obj = new Maybe<T>(typed);
                }
    
                if (!(obj is Maybe<T> other)) return false;
    
                return Equals(other);
            }
    
            /// <summary>
            ///     Good practice when overriding Equals method.
            ///     If x.Equals(y) then we must have x.GetHashCode()==y.GetHashCode()
            /// </summary>
            /// <returns></returns>
            public override int GetHashCode()
            {
                return HasValue ? _value.GetHashCode() : 0;
            }
    
            public override string ToString()
            {
                return HasValue ? _value.ToString() : "NO VALUE";
            }
        }

     این بار می‌توان به امضای متد مذکور اعتماد کرد که قطعا خروجی null ارائه نخواهد داد؛ مگر اینکه به صورت صریح مشخص شود.
    نکته: پیاده سازی صحیحی از واسط IEquatable برای Value Typeها در پیاده سازی struct بالا در نظر گرفته شده است.
    استفاده از آن
    public virtual async Task<Maybe<TModel>> GetByIdAsync(long id)
    {
        Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));
    
        var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
            .ProjectTo<TModel>(_mapper.ConfigurationProvider).SingleOrDefaultAsync();
    
        return entity;
    }
    ساختار داده Maybe تعریف شده در بالا شبیه است با ساختار داده Nullable با این تفاوت که برای انواع ارجاعی مورد استفاده می‌باشد.
    Maybe<T> = Nullable<T>

    • #
      ‫۶ سال و ۱ ماه قبل، پنجشنبه ۴ مرداد ۱۳۹۷، ساعت ۰۰:۴۸
      ممنون از شما ...
      هدف بنده از این مطلب آشنایی با این ایده (maybe or optional )که شما را به دنیای برنامه نویسی تابعی در#C هدایت کند اون هم با ساده‌ترین روش و مثال بود نه پیاده سازی خاصی چون در طول زمان پیاده سازی‌ها متفاوت خواهند شد ولی مفاهیم ثابت می‌مونن.
      این  کتابخانه  و کتابش    هم خوبه اگر دوست داشتید. 
  • #
    ‫۳ سال و ۹ ماه قبل، سه‌شنبه ۱۱ آذر ۱۳۹۹، ساعت ۱۶:۲۱
    سلام. ممنون از مطلب مفید شما.
    شاید علت اینکه option در #C وجود نداره اینه که متد ()<Enumerable.Empty<TResult وجود داره و نیازی به این گزینه نیست. ولی در کل، هم فکر کردن در مورد NRE و هم آشنایی بیشتر با زبان #F مفید است. البته با توجه به اینکه این مقاله قبلا نوشته شده، باید بگم الان کامپایلر #C بسیار قویتر شده و تا حد زیادی جاهایی که احتمال وقوع این exception هست رو به شما خبر میده.