Hãy bắt đầu với một cuộc thảo luận ngắn về đối sánh mẫu trong Ruby, chức năng của nó và cách nó có thể giúp cải thiện khả năng đọc mã.
Nếu bạn giống tôi vài năm trước, bạn có thể nhầm nó với khớp mẫu trong Regex. Ngay cả một tìm kiếm nhanh trên Google về 'đối sánh mẫu' mà không có ngữ cảnh nào khác cũng mang lại cho bạn nội dung khá gần với định nghĩa đó.
Về mặt hình thức, đối sánh mẫu là quá trình kiểm tra bất kỳ dữ liệu nào (có thể là một chuỗi ký tự, một chuỗi mã thông báo, một bộ hoặc bất kỳ thứ gì khác) so với dữ liệu khác.
Về mặt lập trình, tùy thuộc vào khả năng của ngôn ngữ, điều này có thể có nghĩa là bất kỳ điều nào sau đây:
- Khớp với kiểu dữ liệu mong đợi
- Khớp với cấu trúc băm dự kiến (ví dụ:sự hiện diện của các khóa cụ thể)
- Khớp với độ dài mảng dự kiến
- Gán kết quả phù hợp (hoặc một phần của chúng) cho một số biến
Bước đột phá đầu tiên của tôi vào đối sánh mẫu là thông qua Elixir. Elixir có hỗ trợ hạng nhất cho đối sánh mẫu, đến nỗi =
thực tế là toán tử match
toán tử, thay vì chỉ định đơn giản.
Điều này có nghĩa là trong Elixir, đoạn mã sau thực sự hợp lệ:
iex> x = 1
iex> 1 = x
Với ý nghĩ đó, chúng ta hãy xem xét hỗ trợ so khớp mẫu mới cho Ruby 2.7+ và cách chúng ta có thể sử dụng nó để làm cho mã của chúng ta dễ đọc hơn, bắt đầu từ hôm nay.
Phù hợp với mẫu Ruby với case
/ in
Ruby hỗ trợ khớp mẫu với một case
đặc biệt / in
biểu hiện. Cú pháp là:
case <expression>
in <pattern1>
# ...
in <pattern2>
# ...
else
# ...
end
Điều này không được nhầm lẫn với case
/ when
biểu hiện. when
và in
các nhánh không được trộn lẫn trong một case
duy nhất .
Nếu bạn không cung cấp else
biểu thức, bất kỳ kết quả khớp không thành công nào sẽ tạo ra NoMatchingPatternError
.
Mảng đối sánh mẫu trong Ruby
Đối sánh mẫu có thể được sử dụng để so khớp các mảng với cấu trúc được yêu cầu trước dựa trên kiểu dữ liệu, độ dài hoặc giá trị.
Ví dụ:tất cả những điều sau đây đều phù hợp (lưu ý rằng chỉ in
sẽ được đánh giá, là case
ngừng tìm kiếm trận đấu đầu tiên):
case [1, 2, "Three"]
in [Integer, Integer, String]
"matches"
in [1, 2, "Three"]
"matches"
in [Integer, *]
"matches" # because * is a spread operator that matches anything
in [a, *]
"matches" # and the value of the variable a is now 1
end
Loại mệnh đề đối sánh mẫu này rất hữu ích khi bạn muốn tạo ra nhiều tín hiệu từ một lệnh gọi phương thức.
Trong thế giới Elixir, điều này thường được sử dụng khi thực hiện các thao tác có thể có cả :ok
kết quả và một :error
kết quả, ví dụ, được chèn vào cơ sở dữ liệu.
Đây là cách chúng tôi có thể sử dụng nó để dễ đọc hơn:
def create
case save(model_params)
in [:ok, model]
render :json => model
in [:error, errors]
render :json => errors
end
end
# Somewhere in your code, e.g. inside a global helper or your model base class (with a different name).
def save(attrs)
model = Model.new(attrs)
model.save ? [:ok, model] : [:error, model.errors]
end
Đối tượng khớp mẫu trong Ruby
Bạn cũng có thể so khớp các đối tượng trong Ruby để thực thi một cấu trúc cụ thể:
case {a: 1, b: 2}
in {a: Integer}
"matches" # By default, all object matches are partial
in {a: Integer, **}
"matches" # and is same as {a: Integer}
in {a: a}
"matches" # and the value of variable a is now 1
in {a: Integer => a}
"matches" # and the value of variable a is now 1
in {a: 1, b: b}
"matches" # and the value of variable b is now 2
in {a: Integer, **nil}
"does not match" # This will match only if the object has a and no other keys
end
Điều này hoạt động hiệu quả khi áp đặt các quy tắc mạnh mẽ để đối sánh với bất kỳ thông số nào.
Ví dụ:nếu bạn đang viết một lời chào cầu kỳ, nó có thể có cấu trúc sau (rất kiên quyết):
Hàm bămdef greet(hash = {})
case hash
in {greeting: greeting, first_name: first_name, last_name: last_name}
greet(greeting: greeting, name: "#{first_name} #{last_name}")
in {greeting: greeting, name: name}
puts "#{greeting}, #{name}"
in {name: name}
greet(greeting: "Hello", name: name)
in {greeting: greeting}
greet(greeting: greeting, name: "Anonymous")
else
greet(greeting: "Hello", name: "Anonymous")
end
end
greet # Hello, Anonymous
greet(name: "John") # Hello, John
greet(first_name: "John", last_name: "Doe") # Hello, John Doe
greet(greeting: "Bonjour", first_name: "John", last_name: "Doe") # Bonjour, John Doe
greet(greeting: "Bonjour") # Bonjour, Anonymous
Ràng buộc và Ghim biến trong Ruby
Như chúng ta đã thấy trong một số ví dụ trên, đối sánh mẫu thực sự hữu ích trong việc gán một phần của các mẫu cho các biến tùy ý. Đây được gọi là ràng buộc biến và có một số cách chúng ta có thể liên kết với một biến:
- Với kết hợp loại mạnh, ví dụ:
in [Integer => a]
hoặcin {a: Integer => a}
- Không có đặc tả loại, ví dụ:
in [a, 1, 2]
hoặcin {a: a}
. - Không có tên biến, mặc định sử dụng tên khóa, ví dụ:
in {a:}
sẽ xác định một biến có têna
với giá trị tại khóaa
. - Ràng buộc phần còn lại, ví dụ:
in [Integer, *rest]
hoặcin {a: Integer, **rest}
.
Vậy làm cách nào để chúng ta có thể đối sánh khi chúng ta muốn sử dụng một biến hiện có làm mẫu con? Đây là lúc chúng ta có thể sử dụng biến ghim với ^
(pin) toán tử:
a = 1
case {a: 1, b: 2}
in {a: ^a}
"matches"
end
Bạn thậm chí có thể sử dụng điều này khi một biến được xác định trong chính một mẫu, cho phép bạn viết các mẫu mạnh mẽ như sau:
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
puts "both billing and shipping are to the same city"
else
raise "both billing and shipping must be to the same city"
end
Một điều quan trọng cần đề cập với ràng buộc biến là ngay cả khi mẫu không khớp hoàn toàn, biến vẫn sẽ bị ràng buộc. Điều này đôi khi có thể hữu ích.
Tuy nhiên, trong hầu hết các trường hợp, đây cũng có thể là nguyên nhân gây ra các lỗi nhỏ - vì vậy hãy đảm bảo rằng bạn không dựa vào các giá trị biến bị che khuất đã được sử dụng trong một trận đấu. Ví dụ:trong phần sau, bạn sẽ mong đợi thành phố là "Amsterdam", nhưng thay vào đó sẽ là "Berlin":
city = "Amsterdam"
order = {billing_address: {city: "Berlin"}, shipping_address: {city: "Zurich"}}
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
puts "both billing and shipping are to the same city"
else
puts "both billing and shipping must be to the same city"
end
puts city # Berlin instead of Amsterdam
Đối sánh các lớp tùy chỉnh của Ruby
Bạn có thể triển khai một số phương thức đặc biệt để làm cho việc đối sánh mẫu các lớp tùy chỉnh được nhận biết trong Ruby.
Ví dụ:để đối sánh mẫu người dùng với first_name
của anh ta và last_name
, chúng ta có thể xác định deconstruct_keys
trên lớp:
class User
def deconstruct_keys(keys)
{first_name: first_name, last_name: last_name}
end
end
case user
in {first_name: "John"}
puts "Hey, John"
end
Các phím keys
đối số cho deconstruct_keys
chứa các khóa đã được yêu cầu trong mẫu. Đây là cách để người nhận chỉ cung cấp các khóa được yêu cầu nếu việc tính toán tất cả chúng là tốn kém.
Theo cách tương tự như deconstruct_keys
, chúng tôi có thể cung cấp triển khai deconstruct
để cho phép các đối tượng được so khớp theo mẫu dưới dạng một mảng. Ví dụ:giả sử chúng ta có Location
lớp có vĩ độ và kinh độ. Ngoài việc sử dụng deconstruct_keys
để cung cấp các khóa vĩ độ và kinh độ, chúng tôi có thể hiển thị một mảng ở dạng [latitude, longitude]
nữa:
class Location
def deconstruct
[latitude, longitude]
end
end
case location
in [Float => latitude, Float => longitude]
puts "#{latitude}, #{longitude}"
end
Sử dụng Bảo vệ cho các Mẫu phức tạp
Nếu chúng ta có các mẫu phức tạp không thể được biểu diễn bằng các toán tử đối sánh mẫu thông thường, chúng ta cũng có thể sử dụng if
(hoặc unless
) tuyên bố để cung cấp một người bảo vệ cho trận đấu:
case [1, 2]
in [a, b] if b == a * 2
"matches"
else
"no match"
end
Mẫu Khớp với =>
/ in
Không có case
Nếu bạn đang sử dụng Ruby 3+, bạn có quyền truy cập vào nhiều phép thuật so khớp mẫu hơn nữa. Bắt đầu từ Ruby 3, đối sánh mẫu có thể được thực hiện trong một dòng duy nhất mà không cần câu lệnh trường hợp:
[1, 2, "Three"] => [Integer => one, two, String => three]
puts one # 1
puts two # 2
puts three # Three
# Same as above
[1, 2, "Three"] in [Integer => one, two, String => three]
Cho rằng cú pháp trên không có else
, nó hữu ích nhất khi cấu trúc dữ liệu được biết trước.
Ví dụ:mẫu này có thể vừa khít bên trong bộ điều khiển cơ sở chỉ cho phép người dùng quản trị:
class AdminController < AuthenticatedController
before_action :verify_admin
private
def verify_admin
Current.user => {role: :admin}
rescue NoMatchingPatternError
raise NotAllowedError
end
end
Khớp mẫu trong Ruby:Xem không gian này
Thoạt đầu, so khớp mẫu có thể cảm thấy hơi lạ khi nắm bắt, đối với một số người, nó có thể cảm thấy giống như đối tượng được tôn vinh / giải cấu trúc mảng.
Nhưng nếu sự phổ biến của Elixir là bất kỳ dấu hiệu nào, thì khớp mẫu là một công cụ tuyệt vời để có trong kho vũ khí của bạn. / P>
Nếu bạn đang sử dụng Ruby 2.7, đối sánh mẫu (với case
/ in
) vẫn đang thử nghiệm. Với Ruby 3, case
/ in
đã chuyển sang trạng thái ổn định trong khi các biểu thức đối sánh mẫu một dòng mới được giới thiệu là thử nghiệm. Bạn có thể tắt cảnh báo bằng Warning[:experimental] = false
trong mã hoặc -W:no-experimental
phím dòng lệnh.
Mặc dù đối sánh mẫu trong Ruby vẫn đang ở giai đoạn đầu, tôi hy vọng bạn thấy phần giới thiệu này hữu ích và bạn cũng hào hứng như tôi về những phát triển trong tương lai!
Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký nhận bản tin về Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!