12/08/2018, 14:50

Các phương thức thay đổi(mutating) và không thay đổi(non-mutating) trong Ruby

Trong phần trước Truyền tham chiếu, tham trị trong Ruby và một số lưu ý chúng ta đã tìm hiểu cách mà Ruby thao tác với các biến, các biến không thực sự chứa giữ liệu, nhưng thay vào đó nó tham chiếu đến một đối tượng. Chúng ta cũng đã tìm hiểu khái niệm về đối tượng có thể thay đổi và không thể ...

Trong phần trước Truyền tham chiếu, tham trị trong Ruby và một số lưu ý chúng ta đã tìm hiểu cách mà Ruby thao tác với các biến, các biến không thực sự chứa giữ liệu, nhưng thay vào đó nó tham chiếu đến một đối tượng. Chúng ta cũng đã tìm hiểu khái niệm về đối tượng có thể thay đổi và không thể thay đổi, và khái niệm về truyền tham chiếu, tham trị trong Ruby. Trong bài viết này, chúng ta sẽ tìm hiểu về các phương thức, và làm thế nào chúng có thể thay đổi và không thể thay đổi các tham số tương ứng. Chúng ta tập trung đặc biệt vào các phương thức mà thực hiện phép gán hoặc phép nối, hai toán tử này gây nhiều bối rối cho người mới học Ruby.

Phương thức thay đổi và không thay đổi

Các phương thức không thay đổi(Non-Mutating)

Một phương thức được nói là không thay đổi (non-mutating) nếu nó không thay đổi các tham số của nó. Tất cả các phương thức sẽ là non-mutating nếu các tham số là các đối tượng không thể thay đổi. Như vậy, bất kỳ phương thức nào mà các thao tác trên tham số kiểu number và boolean thì sẽ được gọi là phương thức không thay đổi(non-mutating).

Phép gán là không thay đổi

Trước hết ta xét đến phép gán =, phép gán không thay đổi đối tượng, nó đơn giản chỉ là kết nối một biến đến một đối tượng mới. Trong khi phép gán thực tế không phải là một phương thức trong Ruby, nó hành động như một phương thức non-mutating, và cần được đối sử như vậy. Cùng xem qua đoạn mã sau:

>> def fix(value)
--   value.upcase!
--   value.concat('!')
-- end
=> :fix

>> s = 'hello'
=> "hello"

>> s.object_id
=> 70363946430440

>> t = fix(s)
=> "HELLO!"

>> s
=> "HELLO!"

>> t
=> "HELLO!"

>> s.object_id
=> 70363946430440

>> t.object_id
=> 70363946430440

Ta truyền một String vào hàm fix và thực hiện hàm upcase! và concat lên String đó, cuối cùng trả lại một tham chiếu đến String. Từ đoạn code trên ta thấy hàm fix đã làm thay đổi giá trị tham số String mà không thay đổi tham chiếu của tham số truyền vào, nghĩa là hàm upcase! và ham concat đã thay đổi trực tiếp tham số String. Hãy cùng thực hiện một đoạn code khác:

>> def fix(value)
--   value = value.upcase
--   value.concat('!')
-- end
=> :fix

>> s = 'hello'
=> "hello"

>> s.object_id
=> 70349169469400

>> t = fix(s)
=> "HELLO!"

>> s
=> "hello"

>> t
=> "HELLO!"

>> s.object_id
=> 70349169469400

>> t.object_id
=> 70349169435840

s và t giờ đã tham chiếu đến các đối tượng khác nhau, và String được tham chiếu bởi s đã không thay đổi giá trị. Điều gì đã sảy ra ở đây? Chạy đoạn mã sau:

def fix(value)
  puts "initial object #{value.object_id}"
  value = value.upcase
  puts "upcased object #{value.object_id}"
  value.concat('!')
end

s = 'hello'
puts "original object #{s.object_id}"
t = fix(s)
puts "final object #{t.object_id}"

Ta có kết quả:

original object 70349169469400
initial object 70349169469400
upcased object 70349169435840
final object 70349169435840

Từ kết quả trên ta thấy hàm upcase đã không thay đổi trực tiếp trên String truyền vào mà tạo ra một đối tượng mới. Hình sau mô tả rõ kết quả đoạn mã: Phép gán luôn luôn kết nối biến phía bên phải dấu = đến một tham chiếu phía bên phải. Đối tượng gốc mà biến tham chiếu tới là không bao giờ thay đổi.

Mutating method

Một phương thức được gọi là thay đổi(mutating) nếu nó làm thay đổi giá trị của tham số truyền vào hoặc giá trị chính đối tượng gọi đến nó. Lấy ví dụ với phương thức strip! của lớp String, phương thức dùng để xóa khoảng trắng ở đầu và cuối một đối tượng String:

>> s = '   hey   '
=> "   hey   "

>> s.object_id
=> 70101479494960

>> s.strip!
=> "hey"

>> s.object_id
=> 70101479494960

Ở đây chúng ta đã thay đổi một đối tượng String gốc, s đã tham chiếu đến cùng một đối tượng cả trước và sau khi gọi hàm #strip!. Chỉ có giá trị của object bị thay đổi. Nhiều nhưng không phải tất cả những phương thức làm thay đổi(mutate) sử dụng ! như một kí tự cuối của tên phương thức. Nhưng với trường hợp hàm String#concat là một hàm mutating nhưng không có kí tự ! ở cuối tên hàm. Sự không thống nhất này đôi khi gây ra nhầm lẫn.

Phép gán giá trị cho phần tử mảng làm thay đổi giá trị mảng

Phép gán một phần tử của một String, Hash và Array Lấy ví dụ với một mảng:

a = [3, 5, 8] => [3, 5, 8]

a.object_id => 70240541515340

a[1].object_id => 11

a[1] = 9 => 9

a[1].object_id => 19

a => [3, 9, 8]

a.object_id => 70240541515340 Ta thấy đối tượng tham chiếu của a là không thay đổi, nhưng tham chiếu của phần tử con đã thay đổi do phần tử con thuộc kiểu Number là đối tượng không thay đổi trong Ruby.

Phương thức nối(cancatenation) là mutating

Phương thức #<< được sử dụng bởi các đối tượng collections như Array và Hash cũng như String xây dựng phương thức để nối 2 đối tượng với nhau, chúng có tác dụng tương tự như toán tử +=. Tuy nhiên += là non-mutating còn << là mutating. Thực hiện phương thức với String:

>> s = 'Hello'
=> "Hello"

>> s.object_id
=> 70101471465440

>> s << ' World'
=> "Hello World"

>> s
=> "Hello World"

>> s.object_id
=> 70101471465440

Phương thức set là mutating

Thường áp dụng phương thức set để thay đổi thuộc tính của một đối tượng. Ví dụ:

person.name = 'Bill'
person.age = 23

Kết luận

Trong bài viết này chúng ta thấy phương thức trong Ruby có thể là làm thay đổi hoặc không làm thay đổi giá trị của tham số của phương thức đó. Việc hiểu rõ cách làm việc của Ruby giúp chúng ta hạn chế bug trong quá trình làm việc.

Bài viết được dịch từ bài Mutating and Non-Mutating Methods in Ruby Cảm ơn các bạn đã theo dõi

0