12/08/2018, 16:07

Object và Class - Phần 1

Chúng ta được học rằng Ruby là một ngôn ngữ hướng đối tượng, bắt nguồn từ các ngôn ngữ như SamllTalk và Simula. Mỗi giá trị là một đối tượng và tất cả các chương trình Ruby bao gồm một tập hợp các đối tượng và các thông điệp được gửi giữa chúng. Thông thường, chúng ta học về lập trình hướng đối ...

Chúng ta được học rằng Ruby là một ngôn ngữ hướng đối tượng, bắt nguồn từ các ngôn ngữ như SamllTalk và Simula. Mỗi giá trị là một đối tượng và tất cả các chương trình Ruby bao gồm một tập hợp các đối tượng và các thông điệp được gửi giữa chúng. Thông thường, chúng ta học về lập trình hướng đối tượng bằng việc nhìn vào cách sử dụng các đối tượng và những gì nó có thể làm: Cách nó có thể nhóm các giá trị cùng với nhau và hành vi liên quan đến các giá trị đó; Cách mỗi lớp có một trách nhiệm hoặc mục đích duy nhất và cách các lớp khác nhau có thể liên quan với nhau thông qua các đóng gói hoặc kế thừa. Nhưng đối tượng Ruby là gì? Đối tượng chứa những thông tin gì? Nếu chúng ta nhìn đối tượng Ruby bằng kính hiển vi, chúng ta sẽ thấy gì? Có cái gì chuyển động bên trong không và có gì hay về lớp Ruby. Tôi sẽ trả lời những câu hỏi này bằng cách khám phá hoạt động bên trong của Ruby. Bằng cách nhìn vào cách Ruby cài đặt các đối tượng và các lớp, bạn sẽ học được cách sử dụng chúng và viết các chương trình hướng đối tượng sử dụng Ruby.

Bên trong một đối tượng Ruby

Ruby lưu từng đối tượng tùy chỉnh của bạn trong một cấu trúc C gọi là RObject (Ruby 1.9 và 2.0). Ở phía trên của bức hình, là một con trỏ trỏ tới cấu trúc RObject (ở phía trong, Ruby luôn trỏ bất kỳ giá trị nào với một con trỏ VALUE). Bên dưới con trỏ này, cấu trúc ROject chứa một cấu trúc RBasic bên trong và thông tin cụ thể của các đối tượng tùy chỉnh. Phần RBasic chứa thông tin tất cả các giá trị sử dụng: một bộ các giá trị Boolean được gọi là cờ chứa nhiều giá trị nội bộ và một con trỏ lớp được gọi là klass. Con trỏ lớp chỉ ra lớp mà một đối tượng là một instance. Trong phần RObject, Ruby lưu một mảng các biến instance mà mỗi đối tượng chứa bằng cách sử dụng numiv, số lượng các biến instance và ivptr, một con trỏ trỏ tới một mảng các giá trị. Nếu chúng ta định nghĩa cấu trúc đối tượng Ruby bằng thuật ngữ kỹ thuật, chúng ta có thể nói: Mỗi đối tượng Ruby là sự kết hợp của con trỏ lớp và một mảng các biến instance. Thoạt nhìn định nghĩa này có vẻ vô dụng vì nó không giúp chúng ta hiểu được ý nghĩa và mục đích đằng sau đối tượng hoặc cách sử dụng chúng trong chương trình Ruby.

Kiểm tra klass và ivptr

Để hiểu cách Ruby sử dụng các RObject trong chương trình, chúng ta sẽ tạo ra một lớp Ruby đơn giản và sau đó kiểm tra một instance của lớp này bằng IRB.

class Mathematician
  attr_accessor :first_name
  attr_accessor :last_name
end

Ruby cần lưu con trỏ lớp trong RObject vì mỗi đối tượng phải theo dõi lớp bạn đã sử dụng để tạo ra đối tượng. Khi bạn tạo một instance của một lớp, bản thân Ruby sẽ lưu một con trỏ vào lớp bên trong RObject. Ví dụ:

$ irb
> euler = Mathematician.new
=> #<Mathematician:0x007fbd738608c0>

Bằng cách hiển thị tên lớp #<Mathematician, Ruby hiển thị giá trị của con trỏ lớp đối với đối tượng euler. Chuỗi Hex trên kia thực sự là giá trị con trỏ của đối tượng (điều này là khác nhau với mỗi instance của Mathematician). Ruby cũng sử dụng mảng các biến instance để theo dõi các giá trị bạn lưu trong một đối tượng

> euler.first_name = 'Leonhard'
=> "Leonhard" 
> euler.last_name = 'Euler'
=> "Euler" 
> euler
=> #<Mathematician:0x007fbd738608c0 @first_name="Leonhard", @last_name="Euler"> 

Trong IRB, Ruby cũng hiển thị mảng biến instance cho euler tại Mathmatician. Ruby cần lưu lại mảng các giá trị này trong mỗi đối tượng bởi vì mỗi instance đối tượng có thể có các giá trị khác nhau cho cùng một biến instance, ví dụ:

> euclid = Mathematician.new
> euclid.first_name = 'Euclid'
> euclid
=> #<Mathematician:0x007fabdb850690 @first_name="Euclid">

Hình dung hai instance của một lớp

Khi chạy mã dưới đây, chúng ta sẽ tạo ra một cấu trúc RClass và hai cấu trúc RObject (tạo 2 instance của một lớp).

//RClass: Mathematician 
class Mathematician
  attr_accessor :firt_name
  attr_accessor :last_name
end 

//RObject: euler
euler = Mathematician.new
euler.first_name = "Leonhard"
euler.last_name = "Euler"

//RObject: euclid 
euclid = Mathematician.new
euclid.first_name = "Euclid"

Tôi sẽ thảo luận về cách Ruby cài đặt các lớp cấu trúc RClass trong phần tiếp theo. Bây giờ chúng ta sẽ nhìn vào hình dưới, chúng ta sẽ hiểu được việc Ruby lưu các thông tin Mathematician như thế nào trong hai cấu trúc RObject. Như bạn thấy, mỗi giá trị kclass trỏ vào cấu trúc RClass Mathematician và mỗi cấu trúc RObject có một mảng các instance riêng biệt. Cả hai mảng chứa các con trỏ VALUE - cùng một con trỏ mà Ruby dùng để liên kết đến cấu trúc RObject (lưu ý một đối tượng chứa 2 biến instance còn đối tượng còn lại chỉ chứa một biến instance).

Những đối tượng chung

Bây giờ thì bạn đã biết được cách Ruby lưu các lớp như lớp Mathematician, trong cấu trúc RObject. Nhưng hãy nhớ rằng mỗi giá trị trong Ruby - bao gồm dữ liệu cơ bản như là số nguyên, chuỗi và ký tự là một đối tượng. Làm thế nào Ruby có thể lưu trữ các đối tượng chung, nó có sử dụng cấu trúc RObject? Câu trả lời là không. Bên trong Ruby sử dụng cấu trúc C khác, chứ không phải RObject để lưu các giá trị cho kiểu dữ liệu chung của nó. Ví dụ, Ruby lưu các giá trị string trong cấu trúc RString, các mảng trong cấu trúc RArray, regular expression trong cấu trúc RRegexp, ...vv. Ruby sử dụng RObject chỉ để lưu biến instance của lớp đối tượng mà bạn đã tạo và một vài lớp đối tượng mà Ruby đã tạo ra. Tuy nhiên, tất cả các cấu trúc khác nhau chia sẻ cùng một thông tin RBasic mà chúng ta thấy trong RObject được thể hiện trong hình dưới đây. Cấu trúc RBasic chứa con trỏ lớp, mỗi loại dữ liệu chung cũng là một đối tượng. Mỗi một instance của một lớp Ruby được trỏ bởi con trỏ lớp lưu trong RBasic.

Các giá trị Ruby đơn giản không chứa trong bất kỳ cấu trúc nào

Để tối ưu hiệu năng, Ruby đã lưu số nguyên nhỏ, ký hiệu và một vài giá trị đơn giản khác mà không có cấu trúc nào cả, đặt nó ngay trong con trỏ VALUE như hình bên dưới Những giá trị này không phải là con trỏ mà là giá trị của chính bản thân nó. Thay vào đó, Ruby ghi nhớ các lớp bằng cách sử dụng một các bit flag được lưu trong những bit đầu tiên của VALUE. Ví dụ, tất cả các số nguyên nhỏ có cài đặt FIXNUM_FLAG, giống như hình sau Bất cứ khi nào khi thiết lập FIXNUM_FLAG, Ruby đều hiểu rằng VALUE thực sự là một số nguyên nhỏ, một instance của lớp Fixnum và không phải là một con trỏ trỏ tới cấu một cấu trúc giá trị. (Một bit flag tương tự đại diện cho VALUE là một symbol, các giá trị như là nil, true và false cũng có flag riêng của chúng) Thật dễ dàng để thấy integer, string và các giá trị chung khác là tất cả các đối tượng bằng cách sử dụng IRB,

$ irb
> "string".class
=> String
> 1.class
=> Fixnum
> :symbol.class
=> Symbol

Ở đây chúng ta thấy rằng Ruby lưu một con trỏ lớp hoặc một bit flag tương đương cho tất cả các giá trị bằng cách gọi class method. Ngược lại, method class trả về con trỏ lớp hoặc ít nhất là tên của lớp mà mỗi con trỏ klass liên kết đến.

Các đối tượng chung có các biến instance không?

Chúng ta quay lại với định nghĩa của chúng ta về một đối tượng Ruby:

Mỗi đối tượng Ruby là sự kết hợp của một con trỏ lớp và một mảng các biến instance

Bạn nghĩ gì về biến instance của các đối tượng chung? Liệu rằng integer, string và các giá trị dữ liệu chung khác có những biến instance? Điều này có vẻ khó tin, nhưng integer, string thật sự là những đối tượng. Vậy chỗ nào trong Ruby lưu những giá trị này nếu nó không sử dụng cấu trúc RObject? Sử dụng phương thức instance_variables, bạn có thể thấy rằng, mỗi giá trị cơ bản cũng có thể chứa một mảng các biến instance, nghe có vẻ kỳ lạ nhưng đây lại là sự thật

$ irb
> str = "some string value"
=> "some string value"
> str.instance_variables
=> []
> str.instance_variable_set("@val1", "value one")
=> "value one"
> str.instance_variables
=> [:@val1]
> str.instance_variable_set("@val2", "value two")
=> "value two"
> str.instance_variables
=> [:@val1, :@val2]

Lặp lại các thao tác này bằng cách sử dụng các symbol, array hoặc bất kỳ giá trị Ruby nào bạn sẽ nhận ra rằng mỗi giá trị Ruby là một đối tượng. Và mỗi đối tượng có chứa một con trỏ lớp và một mảng các biến instance.

Ruby lưu các biến instance cho các đối tượng chung ở đâu?

Ruby sử dụng một bit của một hack để lưu những biến instance cho các dối tượng chung, nghĩa là đối với các đối tượng không sử dụng cấu trúc RObject. Khi bạn lưu một biến instance trong một đối tượng chung, Ruby lưu nó trong một hash đặc biệt gọi là generic_iv_tbl. Bảng băm này duy trì một map giữa các đối tượng chung và con trỏ đến những bảng băm khác có chứa biến instance của mỗi đối tượng. Hình ảnh sau sẽ cho ta thấy về ví dụ tìm kiếm một string str

Kết luận

Bài viết này chỉ là dịch lại phần Object trong chương 5: Object and Class của của cuốn sách Ruby Under a Microscope Mình sẽ dịch lại phần Class của chương này trong bài viblo tiếp theo. Rất mong nhận được những lời đóng góp ý kiến của mọi người.

0