۱۳۸۸/۰۶/۰۸

استثناي Sequence contains no elements در حين استفاده از LINQ


در ابتدا مثال‌هاي زير را در نظر بگيريد:

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

namespace testWinForms87
{
public class Data
{
public int id { get; set; }
public string name { get; set; }
}

class CLinqTests
{
public static int TestGetListMin1()
{
var lst = new List<Data>
{
new Data{ id=1, name="id1"},
new Data{ id=2, name="id2"},
new Data{ id=3, name="name3"}
};

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}

public static int TestGetListMin2()
{
var lst = new List<Data>();

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
}
}
در متد TestGetListMin1 قصد داريم كوچكترين آي دي ركوردهايي را كه نام آن‌ها حاوي id است، از ليست تشكيل شده از كلاس Data بدست آوريم (همانطور كه مشخص است سه ركورد از نوع Data در ليست lst ما قرار گرفته‌اند).
محاسبات آن كار مي‌كند و مشكلي هم ندارد. اما هميشه در دنياي واقعي همه چيز قرار نيست به اين خوبي پيش برود. ممكن است همانند متد TestGetListMin2 ، ليست ما خالي باشد (براي مثال از ديتابيس، ركوردي مطابق شرايط كوئري‌هاي قبلي بازگشت داده نشده باشد). در اين حالت هنگام فراخواني متد Min ، استثناي Sequence contains no elements رخ خواهد داد و همانطور كه در مباحث defensive programming عنوان شد، وظيفه‌ي ما اين نيست كه خودرو را به ديوار كوبيده (يا منتظر شويم تا كوبيده شود) و سپس به فكر چاره بيفتيم كه خوب، عجب! مشكلي رخ داده است!
اكنون چه بايد كرد؟ حداقل يك مرحله بررسي اينكه آيا كوئري ما حاوي ركوردي مي‌باشد يا خير بايد به اين متد اضافه شود (به صورت زير):

public static int TestGetListMin3()
{
var lst = new List<Data>();
var query = from c in lst
where c.name.Contains("id")
select c.id;

if (query.Any())
return query.Min();
else
return -1;
}
البته مي‌شد اگر هيچ ركوردي بازگشت داده نمي‌شد، يك استثناي سفارشي را ايجاد كرد، اما به شخصه ترجيح مي‌دهم عدد منهاي يك را بر گردانم (چون مي‌دانم ركوردهاي من عدد مثبت هستند و اگر حاصل منفي شد نيازي به ادامه‌ي پروسه نيست).

شبيه به اين مورد در هنگام استفاده از تابع Single مربوط به LINQ نيز ممكن است رخ دهد (توليد استثناي ذكر شده) اما در اينجا مايكروسافت تابع SingleOrDefault را نيز پيش بيني كرده است. در اين حالت اگر كوئري ما ركوردي را برنگرداند، SingleOrDefault مقدار نال را برگشت داده و استثنايي رخ نخواهد داد (نمونه‌ي ديگر آن متدهاي First و FirstOrDefault هستند).
در مورد متدهاي Min و Max ، متدهاي MinOrDefault يا MaxOrDefault در دات نت فريم ورك وجود ندارند. مي‌توان اين نقيصه را با استفاده از extension methods برطرف كرد.

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

public static class LinqExtensions
{
public static T MinOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Min<T>();

return defaultValue;
}

public static T MaxOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Max<T>();

return defaultValue;
}
}
اكنون با استفاده از extension methods فوق، كد ما به صورت زير تغيير خواهد كرد:

public static int TestGetListMin4()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).MinOrDefault(-1);
}