Tại sao bạn nên sử dụng Biểu thức <Func <T >> thay vì Func <T>?


949

Tôi hiểu lambdas và FuncActionđại biểu. Nhưng biểu hiện làm tôi bối rối.

Trong trường hợp nào bạn sẽ sử dụng một Expression<Func<T>>thay vì một cũ đơn giản Func<T>?


14
Func <> sẽ được chuyển đổi thành phương thức ở cấp trình biên dịch c #, Biểu thức <Func <>> sẽ được thực thi ở cấp MSIL sau khi biên dịch mã trực tiếp, đó là lý do nhanh hơn
Waleed AK

1
Ngoài các câu trả lời, đặc tả ngôn ngữ csharp "4.6 loại cây biểu thức" rất hữu ích để tham khảo chéo
djeikyb

Câu trả lời:


1133

Khi bạn muốn coi biểu thức lambda là cây biểu thức và nhìn vào bên trong chúng thay vì thực thi chúng. Ví dụ, LINQ to SQL lấy biểu thức và chuyển đổi nó thành câu lệnh SQL tương đương và gửi nó đến máy chủ (thay vì thực thi lambda).

Về mặt lý thuyết, Expression<Func<T>>hoàn toàn khác nhau từ Func<T>. Func<T>biểu thị một delegatecon trỏ gần giống với một phương thức và Expression<Func<T>>biểu thị cấu trúc dữ liệu cây cho biểu thức lambda. Cấu trúc cây này mô tả những gì một biểu thức lambda làm hơn là thực hiện điều thực tế. Về cơ bản, nó chứa dữ liệu về thành phần của biểu thức, biến, gọi phương thức, ... (ví dụ: nó chứa thông tin như lambda này là một số hằng + một số tham số). Bạn có thể sử dụng mô tả này để chuyển đổi nó thành một phương thức thực tế (với Expression.Compile) hoặc thực hiện các công cụ khác (như ví dụ LINQ sang SQL) với nó. Hành động coi lambdas như các phương thức ẩn danh và cây biểu hiện hoàn toàn là một điều thời gian biên dịch.

Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }

sẽ biên dịch hiệu quả thành một phương thức IL không nhận được gì và trả về 10.

Expression<Func<int>> myExpression = () => 10;

sẽ được chuyển đổi thành cấu trúc dữ liệu mô tả biểu thức không có tham số và trả về giá trị 10:

Biểu hiện vs Func hình ảnh lớn hơn

Trong khi cả hai trông giống nhau ở thời gian biên dịch, những gì trình biên dịch tạo ra lại hoàn toàn khác nhau .


96
Vì vậy, nói cách khác, một Expressionchứa thông tin meta về một đại biểu nhất định.
bertl

40
@bertl Thật ra thì không. Các đại biểu không tham gia vào tất cả. Lý do có bất kỳ mối liên hệ nào với một đại biểu là bạn có thể biên dịch biểu thức cho một đại biểu - hoặc chính xác hơn, biên dịch nó thành một phương thức và lấy đại biểu cho phương thức đó làm giá trị trả về. Nhưng bản thân cây biểu thức chỉ là dữ liệu. Đại biểu không tồn tại khi bạn sử dụng Expression<Func<...>>thay vì chỉ Func<...>.
Luaan

5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }biểu thức như vậy là một ExpressionTree, các nhánh được tạo cho câu lệnh If.
Matteo Marciano - MSCP

3
@bertl Đại biểu là những gì CPU nhìn thấy (mã thực thi của một kiến ​​trúc), Biểu thức là những gì trình biên dịch nhìn thấy (chỉ đơn thuần là một định dạng khác của mã nguồn, nhưng vẫn là mã nguồn).
CodeWarrior

5
@bertl: Nó có thể được tóm tắt chính xác hơn bằng cách nói rằng một biểu thức là một func những gì một người xây dựng chuỗi là một chuỗi. Nó không phải là một chuỗi / func, nhưng nó chứa dữ liệu cần thiết để tạo một dữ liệu khi được yêu cầu làm như vậy.
Flater

337

Tôi đang thêm một câu trả lời vì những câu trả lời này dường như trong đầu tôi, cho đến khi tôi nhận ra nó đơn giản như thế nào. Đôi khi, bạn kỳ vọng rằng nó phức tạp khiến bạn không thể 'quấn đầu quanh nó'.

Tôi không cần phải hiểu sự khác biệt cho đến khi tôi gặp phải một 'lỗi' thực sự khó chịu khi cố gắng sử dụng LINQ-to-SQL một cách tổng quát:

public IEnumerable<T> Get(Func<T, bool> conditionLambda){
  using(var db = new DbContext()){
    return db.Set<T>.Where(conditionLambda);
  }
}

Điều này hoạt động rất tốt cho đến khi tôi bắt đầu nhận OutofMemoryExceptions trên các bộ dữ liệu lớn hơn. Đặt các điểm dừng bên trong lambda khiến tôi nhận ra rằng nó đang lặp đi lặp lại qua từng hàng trong bảng của mình để tìm kiếm các kết quả khớp với điều kiện lambda của tôi. Điều này làm tôi bối rối trong một thời gian, bởi vì tại sao nó lại coi bảng dữ liệu của tôi là một IEn khổng lồ thay vì thực hiện LINQ-to-SQL như nó được yêu cầu? Nó cũng đang làm điều tương tự chính xác trong đối tác LINQ-to-MongoDb của tôi.

Cách khắc phục chỉ đơn giản là biến Func<T, bool>thành Expression<Func<T, bool>>, vì vậy tôi đã hiểu được tại sao nó cần Expressionthay vì Func, kết thúc ở đây.

Một biểu thức chỉ đơn giản là biến một đại biểu thành một dữ liệu về chính nó. Vì vậy, a => a + 1trở thành một cái gì đó như "Ở bên trái có một int a. Ở bên phải bạn thêm 1 vào nó." Đó là nó. Bạn có thể về nhà ngay bây giờ. Nó rõ ràng có cấu trúc hơn thế, nhưng về cơ bản đó thực sự là một cây biểu hiện - không có gì để quấn lấy đầu bạn.

Hiểu được điều đó, điều này trở nên rõ ràng tại sao LINQ-to-SQL cần một ExpressionFunckhông đủ. Funckhông mang theo nó một cách để đi vào chính nó, để thấy sự khó hiểu về cách dịch nó thành một truy vấn SQL / MongoDb / khác. Bạn không thể xem liệu nó đang thực hiện phép cộng hay phép nhân hay phép trừ. Tất cả những gì bạn có thể làm là chạy nó. Expressionmặt khác, cho phép bạn nhìn vào bên trong đại biểu và xem mọi thứ họ muốn làm. Điều này cho phép bạn dịch đại biểu thành bất cứ điều gì bạn muốn, như truy vấn SQL. Funckhông hoạt động vì DbContext của tôi bị mù với nội dung của biểu thức lambda. Do đó, nó không thể biến biểu thức lambda thành SQL; tuy nhiên, nó đã làm điều tốt nhất tiếp theo và lặp lại điều kiện đó qua từng hàng trong bảng của tôi.

Chỉnh sửa: giải thích về câu cuối cùng của tôi theo yêu cầu của John Peter:

IQueryable mở rộng IEnumerable, vì vậy các phương thức của IEnumerable như Where()có được sự quá tải chấp nhận Expression. Khi bạn vượt qua Expressionđiều đó, kết quả là bạn giữ được một IQueryable, nhưng khi bạn vượt qua Func, bạn sẽ rơi trở lại vào cơ sở IEnumerable và kết quả là bạn sẽ nhận được một IEnumerable. Nói cách khác, không cần chú ý, bạn đã biến tập dữ liệu của mình thành một danh sách được lặp lại trái ngược với điều gì đó để truy vấn. Thật khó để nhận ra sự khác biệt cho đến khi bạn thực sự nhìn vào mui xe ở các chữ ký.


2
Chad; Vui lòng giải thích nhận xét này thêm một chút: "Func không hoạt động vì DbContext của tôi bị mù với biểu thức lambda thực sự biến nó thành SQL, vì vậy nó đã làm điều tốt nhất tiếp theo và lặp lại điều kiện đó qua từng hàng trong bảng của tôi . "
John Peters

2
>> Func ... Tất cả những gì bạn có thể làm là chạy nó. Điều đó không chính xác, nhưng tôi nghĩ đó là điểm cần được nhấn mạnh. Funcs / Action sẽ được chạy, Biểu thức sẽ được phân tích (trước khi chạy hoặc thậm chí thay vì chạy).
Konstantin

@Chad Vấn đề ở đây có phải là vấn đề không?: Db. Hãy truy vấn tất cả các bảng cơ sở dữ liệu và sau đó, bởi vì . Tôi nghĩ rằng bạn nhận được OutOfMemoryException bởi vì, mã này đã cố tải toàn bộ bảng vào bộ nhớ (và tất nhiên đã tạo ra các đối tượng). Tôi có đúng không Cảm ơn :)
Bence Végert

104

Một cân nhắc cực kỳ quan trọng trong việc lựa chọn Biểu thức so với Func là các nhà cung cấp IQueryable như LINQ to Entities có thể 'tiêu hóa' những gì bạn chuyển trong Biểu thức, nhưng sẽ bỏ qua những gì bạn vượt qua trong Func. Tôi có hai bài viết trên blog về chủ đề này:

Thông tin thêm về Expression vs Func với Entity FrameworkFalling in Love với LINQ - Phần 7: Expressions and Funcs (phần cuối)


+ l để giải thích. Tuy nhiên, tôi nhận được 'Loại nút biểu thức LINQ' Gọi 'không được hỗ trợ trong LINQ to Entities.' và phải sử dụng ForEach sau khi lấy kết quả.
tymtam

77

Tôi muốn thêm một số lưu ý về sự khác biệt giữa Func<T>Expression<Func<T>>:

  • Func<T> chỉ là một MulticastDelegate trường học cũ bình thường;
  • Expression<Func<T>> là một đại diện của biểu thức lambda dưới dạng cây biểu thức;
  • cây biểu thức có thể được xây dựng thông qua cú pháp biểu thức lambda hoặc thông qua cú pháp API;
  • cây biểu thức có thể được biên dịch cho một đại biểu Func<T>;
  • về mặt lý thuyết, việc chuyển đổi ngược lại là có thể, nhưng đó là một loại dịch ngược, không có chức năng dựng sẵn cho điều đó vì nó không phải là một quá trình đơn giản;
  • cây biểu thức có thể được quan sát / dịch / sửa đổi thông qua ExpressionVisitor;
  • các phương thức mở rộng cho IEnumerable hoạt động với Func<T>;
  • các phương thức mở rộng cho IQueryable hoạt động với Expression<Func<T>>.

Có một bài viết mô tả chi tiết với các mẫu mã:
LINQ: Func <T> vs. Expression <Func <T >> .

Hy vọng nó sẽ hữu ích.


Danh sách đẹp, một lưu ý nhỏ là bạn đề cập rằng chuyển đổi nghịch đảo là có thể, tuy nhiên một nghịch đảo chính xác thì không. Một số siêu dữ liệu bị mất trong quá trình chuyển đổi. Tuy nhiên, bạn có thể dịch ngược nó thành cây Expression tạo ra kết quả tương tự khi được biên dịch lại.
Aidiakapi 13/03/2015

76

Có một lời giải thích triết học hơn về nó từ cuốn sách của Krzysztof Cwalina ( Nguyên tắc thiết kế khung: Các quy ước, thành ngữ và mô hình cho các thư viện .NET có thể tái sử dụng );

Rico Mary

Chỉnh sửa cho phiên bản không có hình ảnh:

Hầu hết các lần bạn sẽ muốn Func hoặc Action nếu tất cả những gì cần xảy ra là chạy một số mã. Bạn cần Biểu thức khi mã cần được phân tích, tuần tự hóa hoặc tối ưu hóa trước khi chạy. Biểu thức là để suy nghĩ về mã, Func / Action là để chạy nó.


10
Vâng đặt. I E. Bạn cần biểu thức khi bạn đang mong đợi Func của mình được chuyển đổi thành một loại truy vấn nào đó. I E. bạn cần database.data.Where(i => i.Id > 0)phải được thực thi như SELECT FROM [data] WHERE [id] > 0. Nếu bạn vừa vượt qua Func, bạn đã cài đặt trình điều khiển cho người lái xe và tất cả những gì có thể làm là SELECT *sau đó, khi nó tải tất cả dữ liệu đó vào bộ nhớ, hãy lặp qua từng bộ lọc và lọc ra mọi thứ với id> 0. Bao bọc Functrong Expressionquyền lực của bạn trình điều khiển để phân tích Funcvà biến nó thành một truy vấn Sql / MongoDb / khác.
Chad Hedgcock

Vì vậy, khi tôi đang lên kế hoạch cho một kỳ nghỉ, tôi sẽ sử dụng Expressionnhưng khi tôi đang nghỉ phép thì sẽ như vậy Func/Action;)
GoldBishop

1
@ChadHedgcock Đây là tác phẩm cuối cùng tôi cần. Cảm ơn. Tôi đã xem xét điều này trong một thời gian, và nhận xét của bạn ở đây đã khiến tất cả các nghiên cứu nhấp chuột.
johnny

37

LINQ là ví dụ điển hình (ví dụ, nói chuyện với cơ sở dữ liệu), nhưng trên thực tế, bất cứ lúc nào bạn quan tâm nhiều hơn đến việc thể hiện những việc cần làm, hơn là thực sự làm việc đó. Ví dụ: tôi sử dụng phương pháp này trong ngăn xếp RPC của protobuf-net (để tránh tạo mã, v.v.) - vì vậy bạn gọi một phương thức với:

string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));

Điều này giải mã cây biểu thức để giải quyết SomeMethod(và giá trị của từng đối số), thực hiện cuộc gọi RPC, cập nhật any ref/ outargs và trả về kết quả từ cuộc gọi từ xa. Điều này chỉ có thể thông qua cây biểu thức. Tôi bao gồm điều này nhiều hơn ở đây .

Một ví dụ khác là khi bạn đang xây dựng các cây biểu thức theo cách thủ công cho mục đích biên dịch sang lambda, như được thực hiện bởi mã toán tử chung .


20

Bạn sẽ sử dụng một biểu thức khi bạn muốn coi hàm của mình là dữ liệu chứ không phải là mã. Bạn có thể làm điều này nếu bạn muốn thao tác mã (dưới dạng dữ liệu). Hầu hết thời gian nếu bạn không thấy cần biểu thức thì có lẽ bạn không cần sử dụng biểu thức.


19

Lý do chính là khi bạn không muốn chạy mã trực tiếp, mà là muốn kiểm tra nó. Điều này có thể vì bất kỳ lý do:

  • Ánh xạ mã tới một môi trường khác (ví dụ: mã C # thành SQL trong Entity Framework)
  • Thay thế các phần của mã trong thời gian chạy (lập trình động hoặc thậm chí các kỹ thuật DRY đơn giản)
  • Xác thực mã (rất hữu ích khi mô phỏng kịch bản hoặc khi thực hiện phân tích)
  • Tuần tự hóa - các biểu thức có thể được tuần tự hóa khá dễ dàng và an toàn, các đại biểu không thể
  • An toàn được gõ mạnh vào những thứ vốn không được gõ mạnh và khai thác kiểm tra trình biên dịch mặc dù bạn đang thực hiện các cuộc gọi động trong thời gian chạy (ASP.NET MVC 5 với Razor là một ví dụ hay)

bạn có thể giải thích thêm một chút về số 5
uowzd01

@ uowzd01 Chỉ cần nhìn vào Dao cạo - nó sử dụng phương pháp này rộng rãi.
Luaan

@Luaan Tôi đang tìm kiếm tuần tự biểu thức nhưng không thể tìm thấy bất cứ điều gì nếu không sử dụng bên thứ ba hạn chế. .Net 4.5 có hỗ trợ tuần tự cây biểu thức không?
vabii

@vabii Không phải tôi biết - và nó thực sự không phải là một ý tưởng tốt cho trường hợp chung. Quan điểm của tôi là về việc bạn có thể viết tuần tự hóa khá đơn giản cho các trường hợp cụ thể mà bạn muốn hỗ trợ, chống lại các giao diện được thiết kế trước thời hạn - tôi đã làm điều đó một vài lần. Trong trường hợp chung, một chuỗi Expressioncó thể không thể nối tiếp như một đại biểu, vì bất kỳ biểu thức nào cũng có thể chứa một lời gọi của một tham chiếu phương thức / ủy nhiệm tùy ý. "Dễ" là tương đối, tất nhiên.
Luaan

15

Tôi không thấy bất kỳ câu trả lời nào đề cập đến hiệu suất. Truyền Func<>s vào Where()hoặc Count()là xấu. Thật tệ. Nếu bạn sử dụng Func<>thì nó sẽ gọi IEnumerablecông cụ LINQ thay vì IQueryableđiều đó có nghĩa là toàn bộ các bảng được kéo vào và sau đó được lọc. Expression<Func<>>nhanh hơn đáng kể, đặc biệt nếu bạn đang truy vấn cơ sở dữ liệu sống trên một máy chủ khác.


Điều này có áp dụng cho truy vấn trong bộ nhớ không?
stt106

@ stt106 Có lẽ là không.
mhenry1384

Điều này chỉ đúng nếu bạn liệt kê danh sách. Nếu bạn sử dụng GetEnumerator hoặc foreach, bạn sẽ không tải ienumerable đầy đủ vào bộ nhớ.
nelsontruran

1
@ stt106 Khi được chuyển đến mệnh đề .Where () của Danh sách <>, Biểu thức <Func <>> được .Compile () gọi vào nó, vì vậy Func <> gần như chắc chắn nhanh hơn. Xem tài liệu tham
khảoource.microsoft.com/#System.Core/System/Linq/ Kẻ
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.