Computer >> Máy Tính >  >> Lập trình >> Ruby

Giới thiệu về Khớp mẫu trong Ruby

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:

  1. Khớp với kiểu dữ liệu mong đợi
  2. 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ể)
  3. Khớp với độ dài mảng dự kiến ​​
  4. 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. whenin 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ăm
def 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:

  1. Với kết hợp loại mạnh, ví dụ:in [Integer => a] hoặc in {a: Integer => a}
  2. Không có đặc tả loại, ví dụ:in [a, 1, 2] hoặc in {a: a} .
  3. 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ên a với giá trị tại khóa a .
  4. Ràng buộc phần còn lại, ví dụ:in [Integer, *rest] hoặc in {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!