Cây biểu cảm cho hình nộm? [đóng cửa]


83

Tôi là hình nộm trong kịch bản này.

Tôi đã cố gắng đọc trên Google những thứ này là gì nhưng tôi không hiểu. Ai đó có thể cho tôi một lời giải thích đơn giản về chúng là gì và tại sao chúng lại hữu ích không?

chỉnh sửa: Tôi đang nói về tính năng LINQ trong .Net.


1
Tôi biết bài đăng này khá cũ, nhưng gần đây tôi đang xem xét Cây biểu hiện. Tôi bắt đầu quan tâm sau khi bắt đầu sử dụng Fluent NHibernate. James Gregory sử dụng rộng rãi những gì được gọi là phản xạ tĩnh và anh ấy có phần giới thiệu: jagregory.com/writings/introduction-to-static-reflection Để xem phản xạ tĩnh và cây biểu thức đang hoạt động, hãy xem mã nguồn Fluent NHibernate ( fluentnhibernate.org ). Nó rất sạch sẽ, và một khái niệm rất tuyệt.
Jim Schubert

Câu trả lời:


88

Lời giải thích tốt nhất về cây biểu hiện mà tôi từng đọc là bài báo này của Charlie Calvert.

Tóm lại;

Cây biểu hiện đại diện cho những gì bạn muốn làm, không phải cách bạn muốn làm.

Hãy xem xét biểu thức lambda rất đơn giản sau:
Func<int, int, int> function = (a, b) => a + b;

Tuyên bố này bao gồm ba phần:

  • Một tuyên bố: Func<int, int, int> function
  • Một toán tử bằng: =
  • Một biểu thức lambda: (a, b) => a + b;

Biến functiontrỏ tới mã thực thi thô biết cách cộng hai số .

Đây là sự khác biệt quan trọng nhất giữa đại biểu và biểu thức. Bạn gọi function(a Func<int, int, int>) mà không bao giờ biết nó sẽ làm gì với hai số nguyên bạn đã chuyển. Nó mất hai và trả về một, đó là hầu hết mã của bạn có thể biết.

Trong phần trước, bạn đã biết cách khai báo một biến trỏ đến mã thực thi thô. Cây biểu thức không phải là mã thực thi , chúng là một dạng cấu trúc dữ liệu.

Bây giờ, không giống như các đại biểu, mã của bạn có thể biết cây biểu thức được dùng để làm gì.

LINQ cung cấp một cú pháp đơn giản để dịch mã thành cấu trúc dữ liệu được gọi là cây biểu thức. Bước đầu tiên là thêm một câu lệnh using để giới thiệu vùng Linq.Expressionstên:

using System.Linq.Expressions;

Bây giờ chúng ta có thể tạo một cây biểu thức:
Expression<Func<int, int, int>> expression = (a, b) => a + b;

Biểu thức lambda giống hệt được hiển thị trong ví dụ trước được chuyển đổi thành một cây biểu thức được khai báo là kiểu Expression<T>. Mã định danh expression không phải là mã thực thi; nó là một cấu trúc dữ liệu được gọi là cây biểu thức.

Điều đó có nghĩa là bạn không thể chỉ gọi một cây biểu thức như bạn có thể gọi một đại biểu, nhưng bạn có thể phân tích nó. Vì vậy, mã của bạn có thể hiểu gì bằng cách phân tích biến expression?

// `expression.NodeType` returns NodeType.Lambda.
// `expression.Type` returns Func<int, int, int>.
// `expression.ReturnType` returns Int32.

var body = expression.Body;
// `body.NodeType` returns ExpressionType.Add.
// `body.Type` returns System.Int32.

var parameters = expression.Parameters;
// `parameters.Count` returns 2.

var firstParam = parameters[0];
// `firstParam.Name` returns "a".
// `firstParam.Type` returns System.Int32.

var secondParam = parameters[1].
// `secondParam.Name` returns "b".
// `secondParam.Type` returns System.Int32.

Ở đây chúng ta thấy rằng có rất nhiều thông tin mà chúng ta có thể nhận được từ một biểu thức.

Nhưng tại sao chúng ta cần điều đó?

Bạn đã biết rằng cây biểu thức là một cấu trúc dữ liệu đại diện cho mã thực thi. Nhưng cho đến nay chúng tôi vẫn chưa trả lời được câu hỏi chính là tại sao một người lại muốn thực hiện một chuyển đổi như vậy. Đây là câu hỏi chúng tôi đã hỏi ở đầu bài viết này, và bây giờ là lúc để trả lời nó.

Truy vấn LINQ to SQL không được thực thi bên trong chương trình C # của bạn. Thay vào đó, nó được dịch sang SQL, được gửi qua một dây và thực thi trên máy chủ cơ sở dữ liệu. Nói cách khác, đoạn mã sau không bao giờ thực sự được thực thi bên trong chương trình của bạn:
var query = from c in db.Customers where c.City == "Nantes" select new { c.City, c.CompanyName };

Đầu tiên nó được dịch sang câu lệnh SQL sau và sau đó được thực thi trên máy chủ:
SELECT [t0].[City], [t0].[CompanyName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[City] = @p0

Mã được tìm thấy trong một biểu thức truy vấn phải được dịch thành một truy vấn SQL có thể được gửi đến một quy trình khác dưới dạng một chuỗi. Trong trường hợp này, quá trình đó xảy ra là một cơ sở dữ liệu máy chủ SQL. Rõ ràng là sẽ dễ dàng hơn nhiều để dịch một cấu trúc dữ liệu như cây biểu thức sang SQL hơn là dịch IL thô hoặc mã thực thi sang SQL. Để phóng đại phần nào độ khó của vấn đề, chỉ cần tưởng tượng bạn đang cố gắng dịch một loạt các số không và số một sang SQL!

Khi đã đến lúc dịch biểu thức truy vấn của bạn sang SQL, cây biểu thức đại diện cho truy vấn của bạn sẽ được tách ra và phân tích, giống như chúng ta đã tách cây biểu thức lambda đơn giản của mình trong phần trước. Được cho là, thuật toán phân tích cú pháp cây biểu thức LINQ thành SQL phức tạp hơn nhiều so với thuật toán chúng ta đã sử dụng, nhưng nguyên tắc thì giống nhau. Khi nó đã phân tích các phần của cây biểu thức, LINQ nghiền ngẫm chúng và quyết định cách tốt nhất để viết một câu lệnh SQL sẽ trả về dữ liệu được yêu cầu.

Cây biểu thức được tạo ra để thực hiện nhiệm vụ chuyển đổi mã chẳng hạn như biểu thức truy vấn thành một chuỗi có thể được chuyển cho một số quy trình khác và thực hiện ở đó. Nó là đơn giản. Không có bí ẩn lớn nào ở đây, không có cây đũa thần nào cần phải vẫy. Người ta chỉ cần lấy mã, chuyển đổi nó thành dữ liệu và sau đó phân tích dữ liệu để tìm ra các phần cấu thành sẽ được dịch thành một chuỗi có thể được chuyển sang một quy trình khác.

Bởi vì truy vấn đến trình biên dịch được gói gọn trong một cấu trúc dữ liệu trừu tượng như vậy, trình biên dịch có thể tự do giải thích nó theo hầu hết mọi cách mà nó muốn. Nó không bị buộc phải thực hiện truy vấn theo một thứ tự cụ thể hoặc theo một cách cụ thể. Thay vào đó, nó có thể phân tích cây biểu thức, khám phá những gì bạn muốn làm và sau đó quyết định cách thực hiện. Ít nhất về lý thuyết, nó có quyền tự do xem xét bất kỳ yếu tố nào, chẳng hạn như lưu lượng mạng hiện tại, tải trên cơ sở dữ liệu, bộ kết quả hiện tại mà nó có sẵn, v.v. Trong thực tế, LINQ to SQL không xem xét tất cả các yếu tố này , nhưng về lý thuyết, nó là miễn phí để làm được khá nhiều thứ nó muốn. Hơn nữa, người ta có thể chuyển cây biểu thức này đến một số mã tùy chỉnh mà bạn viết bằng tay, mã này có thể phân tích và dịch nó thành một thứ gì đó rất khác với những gì được tạo bởi LINQ sang SQL.

Một lần nữa, chúng ta thấy rằng cây biểu thức cho phép chúng ta biểu diễn (thể hiện?) Những chúng ta muốn làm. Và chúng tôi sử dụng các trình dịch để quyết định cách diễn đạt của chúng tôi được sử dụng.


2
Một trong những câu trả lời tốt hơn.
johnny

4
câu trả lời xuất sắc. Một khía cạnh nhỏ cần thêm vào lời giải thích tuyệt vời này là - một công dụng khác của cây biểu thức là bạn có thể sửa đổi cây biểu thức ngay lập tức tại thời điểm chạy vì bạn có thể thấy phù hợp trước khi nạp nó được thực thi, điều này đôi khi cực kỳ hữu ích.
Yan D

41

Cây biểu thức là một cơ chế để dịch mã thực thi thành dữ liệu. Sử dụng cây biểu thức, bạn có thể tạo ra một cấu trúc dữ liệu đại diện cho chương trình của bạn.

Trong C #, bạn có thể làm việc với cây biểu thức được tạo bởi biểu thức lambda bằng cách sử dụng Expression<T>lớp.


Trong một chương trình truyền thống, bạn viết mã như sau:

double hypotenuse = Math.Sqrt(a*a + b*b);

Đoạn mã này khiến trình biên dịch tạo ra một nhiệm vụ và thế là xong. Trong hầu hết các trường hợp, đó là tất cả những gì bạn quan tâm.

Với mã thông thường, ứng dụng của bạn không thể quay ngược trở lại và xem xét hypotenuseđể xác định rằng nó được tạo ra bằng cách thực hiện một Math.Sqrt()cuộc gọi; thông tin này đơn giản không phải là một phần của những gì được bao gồm.

Bây giờ, hãy xem xét một biểu thức lambda như sau:

Func<int, int, int> hypotenuse = (a, b) => Math.Sqrt(a*a + b*b);

Điều này hơi khác so với trước đây. Bây giờ hypotenusethực sự là một tham chiếu đến một khối mã thực thi . Nếu bạn gọi

hypotenuse(3, 4);

bạn sẽ nhận được giá trị 5trả về.

Chúng ta có thể sử dụng cây biểu thức để khám phá khối mã thực thi được tạo ra. Hãy thử cái này thay thế:

Expression<Func<int, int, int>> addTwoNumbersExpression = (x, y) => x + y;
BinaryExpression body = (BinaryExpression) addTwoNumbersExpression.Body;
Console.WriteLine(body);

Điều này tạo ra:

(x + y)

Có thể thực hiện các kỹ thuật và thao tác nâng cao hơn với cây biểu hiện.


7
OK, tôi đã ở bên bạn cho đến khi kết thúc nhưng tôi vẫn không thực sự hiểu tại sao đây là một vấn đề lớn. Tôi đang gặp khó khăn khi nghĩ về các ứng dụng.

1
Anh ấy đang sử dụng một ví dụ đơn giản hóa; sức mạnh thực sự nằm ở thực tế là mã của bạn khám phá cây biểu thức, cũng có thể được thực hiện chịu trách nhiệm giải thích nó và áp dụng ý nghĩa ngữ nghĩa cho biểu thức.
Pierreten

2
Có, câu trả lời này sẽ tốt hơn nếu anh ấy / cô ấy giải thích tại sao (x + y) thực sự hữu ích cho chúng tôi. Tại sao chúng ta muốn khám phá (x + y) và làm cách nào để làm điều đó?
Paul Matthews

Bạn không cần phải khám phá nó, bạn làm điều đó chỉ để xem truy vấn của bạn là gì và sẽ được dịch sang một số ngôn ngữ khác trong trường hợp đó to SQL
stanimirsp

15

Cây biểu thức là một biểu diễn trong bộ nhớ của một biểu thức, ví dụ như biểu thức số học hoặc boolean. Ví dụ, hãy xem xét biểu thức số học

a + b*2

Vì * có ưu tiên toán tử cao hơn +, cây biểu thức được xây dựng như vậy:

    [+]
  /    \
 a     [*]
      /   \
     b     2

Có cây này, nó có thể được đánh giá cho bất kỳ giá trị nào của a và b. Ngoài ra, bạn có thể biến đổi nó thành các cây biểu thức khác, chẳng hạn để lấy biểu thức.

Khi bạn triển khai một cây biểu thức, tôi khuyên bạn nên tạo một Biểu thức lớp cơ sở . Bắt nguồn từ đó, lớp BinaryExpression sẽ được sử dụng cho tất cả các biểu thức nhị phân, chẳng hạn như + và *. Sau đó, bạn có thể giới thiệu VariableReferenceExpression cho các biến tham chiếu (chẳng hạn như a và b) và một lớp khác ConstantExpression (cho 2 từ ví dụ).

Trong nhiều trường hợp, cây biểu thức được xây dựng như là kết quả của việc phân tích cú pháp một đầu vào (từ người dùng trực tiếp hoặc từ một tệp). Để đánh giá cây biểu thức, tôi khuyên bạn nên sử dụng mẫu Khách truy cập .


15

Câu trả lời ngắn gọn: Thật tuyệt khi có thể viết cùng một loại truy vấn LINQ và trỏ nó vào bất kỳ nguồn dữ liệu nào. Bạn không thể có truy vấn "Tích hợp ngôn ngữ" nếu không có nó.

Câu trả lời dài: Như bạn có thể biết, khi bạn biên dịch mã nguồn, bạn đang chuyển đổi nó từ ngôn ngữ này sang ngôn ngữ khác. Thông thường từ ngôn ngữ cấp cao (C #) đến cần gạt thấp hơn trên (IL).

Về cơ bản có hai cách bạn có thể làm điều này:

  1. Bạn có thể dịch mã bằng cách sử dụng tìm và thay thế
  2. Bạn phân tích cú pháp mã và nhận được một cây phân tích cú pháp.

Phần sau là những gì mà tất cả các chương trình mà chúng ta gọi là 'trình biên dịch' làm.

Khi bạn có một cây phân tích cú pháp, bạn có thể dễ dàng dịch nó sang bất kỳ ngôn ngữ nào khác và đây là những gì cây biểu thức cho phép chúng tôi thực hiện. Vì mã được lưu trữ dưới dạng dữ liệu nên bạn có thể làm bất cứ điều gì bạn muốn nhưng có thể bạn sẽ chỉ muốn dịch nó sang một số ngôn ngữ khác.

Bây giờ, trong LINQ to SQL, các cây biểu thức được chuyển thành một lệnh SQL và sau đó được gửi qua dây tới máy chủ cơ sở dữ liệu. Theo như tôi biết họ không làm bất cứ điều gì thực sự ưa thích khi dịch mã nhưng họ có thể . Ví dụ: nhà cung cấp truy vấn có thể tạo mã SQL khác nhau tùy thuộc vào điều kiện mạng.


6

IIUC, một cây biểu thức tương tự như một cây cú pháp trừu tượng, nhưng một biểu thức thường mang lại một giá trị duy nhất, trong khi AST có thể đại diện cho toàn bộ chương trình (với các lớp, gói, hàm, câu lệnh, v.v.)

Dù sao, đối với biểu thức (2 + 3) * 5, cây là:

    *
   / \ 
  +   5
 / \
2   3

Đánh giá đệ quy từng nút (từ dưới lên) để nhận giá trị tại nút gốc, tức là giá trị của biểu thức.

Tất nhiên, bạn cũng có thể có toán tử một bậc (phủ định) hoặc ba bậc (nếu-thì-khác) và các hàm (n-ary, tức là bất kỳ số ops nào) nếu ngôn ngữ biểu thức của bạn cho phép.

Đánh giá loại và thực hiện kiểm soát loại được thực hiện trên các cây tương tự.


5

Cây
biểu thức DLR là một bổ sung cho C # để hỗ trợ Thời gian chạy ngôn ngữ động (DLR). DLR cũng là thứ chịu trách nhiệm cung cấp cho chúng ta phương thức khai báo biến "var". ( var objA = new Tree();)

Thông tin thêm về DLR .

Về cơ bản, Microsoft muốn mở CLR cho các ngôn ngữ động, chẳng hạn như LISP, SmallTalk, Javascript, v.v. Để làm được điều đó, họ cần có khả năng phân tích cú pháp và đánh giá các biểu thức một cách nhanh chóng. Điều đó đã không thể xảy ra trước khi DLR ra đời.

Quay lại câu đầu tiên của tôi, Cây biểu thức là một phần bổ sung cho C # mở ra khả năng sử dụng DLR. Trước đó, C # là một ngôn ngữ tĩnh hơn nhiều - tất cả các kiểu biến phải được khai báo là một kiểu cụ thể và tất cả mã phải được viết tại thời điểm biên dịch.

Sử dụng nó với
cây Biểu thức Dữ liệu sẽ mở ra các cửa lũ tới mã động.

Ví dụ, giả sử rằng bạn đang tạo một trang web bất động sản. Trong giai đoạn thiết kế, bạn biết tất cả các bộ lọc mà bạn có thể áp dụng. Để triển khai mã này, bạn có hai lựa chọn: bạn có thể viết một vòng lặp so sánh mỗi điểm dữ liệu với một loạt các lần kiểm tra Nếu-Thì; hoặc bạn có thể cố gắng tạo một truy vấn bằng ngôn ngữ động (SQL) và chuyển nó cho một chương trình có thể thực hiện tìm kiếm cho bạn (cơ sở dữ liệu).

Với cây Biểu thức, giờ đây bạn có thể thay đổi mã trong chương trình của mình - một cách nhanh chóng - và thực hiện tìm kiếm. Cụ thể, bạn có thể thực hiện việc này thông qua LINQ.

(Xem thêm: MSDN: Cách sử dụng: Sử dụng cây biểu thức để xây dựng truy vấn động ).

Ngoài dữ liệu
Công dụng chính của Cây Biểu thức là để quản lý dữ liệu. Tuy nhiên, chúng cũng có thể được sử dụng cho mã được tạo động. Vì vậy, nếu bạn muốn một hàm được định nghĩa động (ala Javascript), bạn có thể tạo Cây biểu thức, biên dịch nó và đánh giá kết quả.

Tôi sẽ đi sâu hơn một chút, nhưng trang web này hoạt động tốt hơn nhiều:

Cây biểu hiện dưới dạng trình biên dịch

Các ví dụ được liệt kê bao gồm tạo toán tử chung cho các kiểu biến, biểu thức lambda cuộn bằng tay, sao chép nông hiệu suất cao và sao chép động các thuộc tính đọc / ghi từ đối tượng này sang đối tượng khác.

Tóm tắt
Cây biểu thức là các đại diện của mã được biên dịch và đánh giá trong thời gian chạy. Chúng cho phép các kiểu động, rất hữu ích cho thao tác dữ liệu và lập trình động.


Vâng, tôi biết mình đến muộn với trò chơi, nhưng tôi muốn viết câu trả lời này như một cách để tự hiểu nó. (Câu hỏi này xuất hiện nhiều trong tìm kiếm trên internet của tôi.)
Richard

Công việc tốt. Đó là một câu trả lời tốt.
Rich Bryant

5
Từ khóa "var" không liên quan gì đến DLR. Bạn đang nhầm lẫn nó với "động".
Yarik

Đây là một câu trả lời hay, nhỏ trên var ở đây, cho thấy Yarik là đúng. Tuy nhiên, cảm ơn vì phần còn lại của câu trả lời. quora.com/…
johnny

1
Tất cả đều sai. varlà một đường cú pháp thời gian biên dịch - nó không liên quan gì đến cây biểu thức, DLR hoặc thời gian chạy. var i = 0được biên dịch như thể bạn đã viết int i = 0, vì vậy bạn không thể sử dụng varđể biểu diễn một kiểu không được biết trong thời gian biên dịch. Cây biểu thức không phải là "một bổ sung để hỗ trợ DLR", chúng được giới thiệu trong .NET 3.5 để cho phép LINQ. Mặt khác, DLR được giới thiệu trong .NET 4.0 để cho phép các ngôn ngữ động (như IronRuby) và dynamictừ khóa. Cây biểu thức thực sự được sử dụng bởi DLR để cung cấp tương tác, không phải ngược lại.
Şafak Gür

-3

Cây biểu hiện mà bạn đang đề cập có phải là cây Đánh giá biểu thức không?

Nếu có thì nó là cây được xây dựng bởi trình phân tích cú pháp. Parser đã sử dụng Lexer / Tokenizer để xác định các Token từ chương trình. Trình phân tích cú pháp xây dựng cây nhị phân từ các mã thông báo.

Đây là lời giải chi tiết


Chà, mặc dù đúng là Cây biểu thức mà OP đề cập đến hoạt động tương tự và có cùng khái niệm cơ bản như cây phân tích cú pháp, nó được thực hiện động tại thời điểm chạy với mã, tuy nhiên lưu ý khi anh ấy giới thiệu trình biên dịch Roslyn dòng sự phân chia giữa hai thực sự bị mờ nếu không được loại bỏ hoàn toàn.
yoel halb
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.