Effective C# 38.偏好 lambda 表達式而非方法 Effective C# 38.偏好 lambda 表達式而非方法 (Prefer Lambda Expressions to Methods)

Published on Thursday, October 24, 2024

這個做法是與寫法相關,建議是用 Lazy Evaluation 的延遲特性來處理需要共用邏輯的場合而不是寫一個方法把共用邏輯放進去。

例如下面這段程式碼用來查詢員工的薪資,這兩個語句只有年資不同其它條件都相同。

void Main()
{
	var allEmployees = FindAllEmployees();
	var earlyFolks = from e in allEmployees
					 where e.Classification == EmployeeType.Salary
					 where e.YearsOfService > 20
					 where e.MonthlySalary < 4000
					 select e;
	var newest = from e in allEmployees
				 where e.Classification == EmployeeType.Salary
				 where e.YearsOfService < 2
				 where e.MonthlySalary < 4000
				 select e;

	Console.WriteLine(earlyFolks);
	Console.WriteLine(newest);
}

public IEnumerable<Employee> FindAllEmployees()
{
	var result = new List<Employee>
	{
		new Employee{
			Name = "User1",
			Classification = EmployeeType.Salary,
			MonthlySalary = 1000,
			YearsOfService = 1
		},
		new Employee{
			Name = "User2",
			Classification = EmployeeType.Salary,
			MonthlySalary = 3000,
			YearsOfService = 21
		},
		new Employee{
			Name = "User3",
			Classification = EmployeeType.Salary,
			MonthlySalary = 1500,
			YearsOfService = 1
		}
	};

	foreach (var element in result)
	{
		yield return element;
	}
}

public class Employee
{
	public string Name { get; set; }
	public EmployeeType Classification { get; set; }
	public int YearsOfService { get; set; }
	public decimal MonthlySalary { get; set; }
}

public enum EmployeeType
{
	Salary
}

所以秉持著不重複的原則可能會寫出下面這種程式碼,也就是把共用的邏輯移動到 LowPaidSalaried 方法裡面,這樣未來如果需求改變了只需要改動 LowPaidSalaried 的邏輯就好,但是這種寫法實際上執行起來會比第一種寫法還要慢。

private static bool LowPaidSalaried(Employee e) =>
      e.MonthlySalary < 4000 && 
      e.Classification == EmployeeType.Salary;
      
var allEmployees = FindAllEmployees();
var earlyFolks = from e in allEmployees
     where LowPaidSalaried(e) && e.YearsOfService > 20
     select e;
var newest = from e in allEmployees
    where LowPaidSalaried(e) &&  e.YearsOfService < 2
    select e;

要改善這種情況可以利用 Lazy Evaluation 的延遲特性把共用的語句編寫成擴充方法,這樣既可以讓程式碼看起來整潔也不會對效能造成太大影響。

void Main()
{
	var allEmployees = FindAllEmployees();
	// Find the first employees:
	var salaried = allEmployees.LowPaidSalariedFilter();
	var earlyFolks = salaried.Where(e => e.YearsOfService > 20);
	// find the newest people:
	var newest = salaried.Where(e => e.YearsOfService < 2);


	Console.WriteLine(earlyFolks);
	Console.WriteLine(newest);
}

public static class MyExtension
{
	public static IEnumerable<Employee> LowPaidSalariedFilter
	(this IEnumerable<Employee> sequence) => from s in sequence
											where s.Classification == EmployeeType.Salary &&
											s.MonthlySalary < 4000
											select s;
}

上面用到的都是 IEnumerable<T> 它背後會把 lambda 轉換成委派 LINQ to Objects 就是這種運作模式,它通常是用來操作記憶體內保存的物件。

另一種則是需要進行額外轉換的 IQueryable<T> 它背後會把 lambda 轉換成 expression tree,LINQ to SQL 就是這種運作模式, 它通常是用來查詢資料庫會用到的,因為它會把我們寫的語句整理起來後翻譯成 SQL 語法丟給資料庫執行。


Summary

這個做法的關鍵是把一個大型的查詢語句拆分成多個可共用的小語句,這個流程很適合用來建立 filter,特別是在資料庫讀取的過程中我們可以建立 IQueryable 的共用版本讓過濾的方法也能進行共用,這樣就可以在發送給資料庫前把條件加入到語句中。