Tôi hiểu lambdas và Func
và Action
đạ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>
?
Tôi hiểu lambdas và Func
và Action
đạ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>
?
Câu trả lời:
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>>
là hoàn toàn khác nhau từ Func<T>
. Func<T>
biểu thị một delegate
con 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:
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 .
Expression
chứa thông tin meta về một đại biểu nhất định.
Expression<Func<...>>
thay vì chỉ Func<...>
.
(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.
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 Expression
thay 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 + 1
trở 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 Expression
và Func
không đủ. Func
khô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ó. Expression
mặ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. Func
khô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ý.
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 Framework và Falling in Love với LINQ - Phần 7: Expressions and Funcs (phần cuối)
Tôi muốn thêm một số lưu ý về sự khác biệt giữa Func<T>
và 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;Func<T>
;ExpressionVisitor
;Func<T>
;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.
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 );
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ó.
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 Func
trong Expression
quyền lực của bạn trình điều khiển để phân tích Func
và biến nó thành một truy vấn Sql / MongoDb / khác.
Expression
nhưng khi tôi đang nghỉ phép thì sẽ như vậy Func/Action
;)
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
/ out
args 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 .
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.
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:
Expression
có 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.
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 IEnumerable
cô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.