Thứ Tư, 30 tháng 1, 2019

[System Verilog][Class]Bài 4 - super và this

super và this là các keyword sử dụng trong việc xây dựng class. Vai trò của các keyword này là gì? bài viết này sẽ giải đáp chi tiết câu hỏi này.
Lưu ý, bạn đọc tham khảo các bài 1, bài 2bài 3 về class trong System Verilog (SV) trước khi đọc bài này vì nhiều thuật ngữ cơ bản trong class đã được giải thích ở các bài trước nên chúng không được giải thích lại trong bài này. Ví dụ, "đối tượng" = class instance.

1) super
super được sử dụng trong một class mở rộng (subclass) để tham khảo (gọi và sử dụng) các thành phần của class gốc (base class). Như vậy, ví trí sử dụng super là trong một class được mở rộng từ một class khác (có dùng extends). super không được sử dụng trong một class không phải là class mở rộng.
Ví dụ 1 - class gốc (base class)
class rand_packet;
 //
 // 1: Declare the class properties
 //
 integer num;
 integer rand_data [];
 integer i = 0;
 //
 // 2: Constructor
 //
 function new;
   begin      num = 1;
      i = 0;
   end  endfunction //
 // 3: Build the class methods (tasks or functions)
 //
 // Method 1: Task in class (object method)
 // Create the random data and store to an array element
 task build_data ();
   begin     rand_data = new[num]; //Set the size of dynamic array
     for (i = 0; i < num; i++) begin       rand_data[i] = $random; //Assign a random value to an array entry
     end   end endtask: build_data
 //
 // Method 2: Task in class (object method)
 // Display all values of array
 task print ();
   begin     for (i=0; i < num; i ++) begin       $display("rand_data[%d] %x",i, rand_data[i]);
     end   end  endtask: print
 //
 // Method 3: Function in class (object method)
 // Get the size of the array
 function integer get_size();
   begin     get_size = num;
   end endfunction: get_size
   
endclass: rand_packet
Ví dụ 2 - class mở rộng (subclass)
class rand_packet_mdf extends rand_packet;

 integer rand_addr [];

 function new;
   begin     num      = 3;
   end endfunction
 task build_packet ();
   begin     //Create the random data packet like base class
     super.build_data;     //Create the random address
     rand_addr = new[num];
     for (i = 0; i < num; i++) begin       rand_addr[i] = $random;
       rand_addr[i] = rand_addr[i] & 32'h0000_fffc;
     end   end endtask: build_packet

 task print();
   for (i=0; i < num; i ++) begin  $display("rand_addr[%d] %x - rand_data[%d] %x",i, rand_addr[i], i, rand_data[i]);
   end endtask: print
endclass: rand_packet_mdf
Ví dụ 2 là một class mở rộng từ ví dụ 1. Trong ví dụ 2, method build_packet được tạo ra với mục đích tạo 1 mảng dữ liệu ngẫu nhiên và 1 mảng địa chỉ ngẫu nhiên có số lượng như nhau. Do class gốc đã có method build_data giúp tạo mảng dữ liệu ngẫu nhiên nên để "super.build_data" được sử dụng để gọi method build_data của class gốc. Sau dòng code này, đoạn code tạo mảng địa chỉ sẽ tạo ra các địa chỉ có 16 bit cao luôn bằng 0 và 16 bit thấp luôn align theo đơn vị 4 (2 bit cuối luôn bằng 0).
Ngoài ra, rand_packet_mdf định nghĩa lại method print để in ra cả giá trị của mảng địa chỉ rand_addr và mảng dữ liệu rand_data. Chú ý, print của rand_packet_mdf sẽ ghi đè lên print của class gốc rand_packet.
Thực thi đoạn code sau đây để kiểm chứng kết quả:
Ví dụ 3 - Code kiểm chứng hoạt động của super
module class_ex_super_this;
   rand_packet_mdf  pkt_mdf;
   initial begin     pkt_mdf = new;
     pkt_mdf.build_packet;
     $display ("Size of packet pkt_mdf: %0d",pkt_mdf.get_size);
     pkt_mdf.print;
   end
endmodule
Hình 1: Kết quả mô phỏng của ví dụ 3
Kết quả mô phỏng cho thấy khi gọi và thực thi "pkt_mdf.build_packet" thì mảng dữ liệu và mảng địa chỉ được tạo ra như mong muốn.
Hình 2: Mối tương quan của super.build_data
Như đã phân tích, super.build_data sẽ gọi method của class gốc rand_packet nên cũng sẽ sử dụng giá trị num khởi tạo trong class gốc. Nhưng "Tại sao super.build_data ở ví dụ 2 sử dụng giá trị num = 3 (gán trong class rand_packet_mdf) mà không sử dụng num = 1 (gán trong class rand_packet)?". Bạn đọc có thể tự suy nghĩ và đưa ra đáp án trước khi đọc phần giải thích ở cuối bài viết.
Trong trường hợp sử dụng đã nêu, build_packet là một method mới nên không cần sử dụng từ khóa super mà chỉ cần gọi build_data là đủ.
Trong trường hợp bị "ghi đè" (overridden), super phải được sử dụng. Ví dụ, method build_packet của ví dụ 2 được đổi tên thành build_data. Tên này trùng với tên trong class gốc nên method build_data của rand_packet sẽ bị ghi đè bởi method build_data trong rand_packet_mdf. Lúc này, super.build_data phải được dùng thì mới gọi được method của class gốc.
super chỉ áp dụng được 1 cấp. không có tác dụng nhiều cấp. Việc cố tình sử dụng nhiều cấp sẽ bị báo lỗi khi biên dịch code.
Ví dụ 4 - Sử dụng super nhiều cấp (vi phạm)
class rand_packet_mdf_lv2 extends rand_packet_mdf;

 task build_packet_new ();
   super.super.build_data; //illegal
 endtask: build_packet_new
endclass: rand_packet_mdf_lv2
Ví dụ trên là một class được mở rộng từ class rand_packet_mdf. Trong ví dụ này, super.super.build_data được sử dụng với mong muốn gọi method build_data từ class rand_packet nhưng hành vi này bị cấm trong SV. Nếu muốn sử dụng method build_data thì chỉ cần super.build_data là đủ (xem ví dụ 5) vì rand_packet_mdf được mở rộng từ rand_paket nên rand_packet_mdf sẽ kế thừa các thành phần của rand_packet, trong đó có build_data.
Ví dụ 5 - Sửa lại ví dụ 4
class rand_packet_mdf_lv2 extends rand_packet_mdf;

 task build_packet_new ();
   super.build_data; //legal
 endtask: build_packet_new
endclass: rand_packet_mdf_lv2
Bên cạnh đó, nếu build_packet_new chỉ sử dụng lại build_data như ví dụ trên thì không cần phải tạo thêm build_packet_new  vì rand_packet_mdf_lv2 sẽ kế thừa từ rand_packet_mdf. Trong khi đó, rand_packet_mdf kế thừa từ rand_packet nên rand_packet_mdf_lv2 cũng sẽ có method build_data.
Ví dụ 6 - Kiểm chứng kết quả ví dụ 4
module class_ex_super_this;
   rand_packet_mdf_lv2 pkt_mdf_lv2;
   initial begin        pkt_mdf_lv2 = new;
     pkt_mdf_lv2.build_data;     $display ("First");
     pkt_mdf_lv2.print;
     //
     pkt_mdf_lv2.build_packet_new;     $display ("Second");
     pkt_mdf_lv2.print;
   end
endmodule
Hình 3: Kết quả mô phỏng của ví dụ 6
Trong ví dụ trên, kết quả khi sử dụng pkt_mdf_lv2.build_datapkt_mdf_lv2.build_packet_new là như nhau. Chú ý, phần giá trị của rand_addr là "x" bởi vì method build_data không có phần code tạo giá trị cho rand_addr.
----------------------------------------------------------------------------------------
Câu hỏi: "Tại sao super.build_data ở ví dụ 2 sử dụng giá trị num = 3 (gán trong class rand_packet_mdf) mà không sử dụng num = 1 (gán trong class rand_packet)?"
Trả lời: Vì biến num trong class gốc rand_packet và class mở rộng rand_packet_mdf là một. Chú ý, function new của subclass rand_packet_mdf chỉ gán lại giá trị đầu cho biến num trước đó (đã khai báo trong base class).
----------------------------------------------------------------------------------------
Nếu thêm vào class rand_packet_mdf dòng khai báo "integer num;" như ví dụ sau đây.
Ví dụ 6 - Ghi đè propety trong base class
class rand_packet_mdf extends rand_packet;
 ...
 integer num;

 function new;
   begin     num      = 3;
   end endfunction...
endclass: rand_packet_mdf
Chạy lại đoạn code kiểm chứng kết quả trong ví dụ 3, kết quả như sau:
Hình 4: Kết quả mô phỏng cho class ví dụ 6 khi chạy code kiểm chứng ở ví dụ 3
Trường hợp này, num trong rand_packet_mdf được khai báo lại (xem như biến mới) nên method build_packet print sẽ dùng giá trị num = 3 trong rand_packet_mdf còn method build_dataget_size sẽ dùng giá trị num = 1 trong rand_packet. Vì vậy, kết quả hình 4 hiển thị "Size of packet pkt_mdf: 1" vì get_size dùng num = 1. rand_data chỉ có 1 giá trị vì "super.build_data" dùng num = 1. rand_addr có 3 giá trị vì build_packet dùng num = 3.
2) this
this là keyword của SV. Nó được sử dụng để tham khảo đến thành phần (property và method) của class trong đối tượng (class instance) hiện tại. this được hiểu như một handle của đối tượng nhưng handle này không sử dụng bên ngoài một đối tượng mà chỉ sử dụng trong các method của chính đối tượng đó.
Ví dụ 7 - class không sử dụng this
class write_packet;
 bit [31:0] addr;
 bit [63:0] wdata;
 bit we;
 bit sel;
 function new (bit [31:0] user_addr, bit [63:0] user_wdata, bit user_we, bit user_sel);
   begin      addr[31:0]  = user_addr[31:0];
      wdata[63:0] = user_wdata[63:0];
      we  = user_we;
      sel = user_sel;
   end
 endfunction
 task print;
   $display ("Write packet: addr=%x, wdata=%x, we=%1x, sel=%1x", addr, wdata, we, sel);
 endtask: print
//
endclass: write_packet
module class_ex_this;
  write_packet wpkt;
  initial begin    wpkt = new(32'h0000_fffc, 64'h8888_9999_AAAA_BBBB, 1'b1, 1'b1);
    wpkt.print;
  endendmodule: class_ex_this
Trong ví dụ trên, write_packet là một class cho phép người dùng gán các giá trị khởi tạo cho addr, wdata, wesel khi tạo một đối tượng mới. Ở đây, khi tạo một đối tượng có handle là wpkt trong module class_ex_this, giá trị các đối số user_addr, user_wdata, user_we user_sel được gán các giá trị mong muốn và truyền đến các biến của class. Kết quả mô phỏng như sau:
Hình 5: Kết quả mô phỏng của ví dụ 7
Bây giờ, bạn hãy thay đoạn code của function new (constructor) trong ví dụ 7 bằng đoạn code sau:
Ví dụ 8 - constructor sử dụng this
 function new (bit [31:0] user_addr, bit [63:0] user_wdata, bit user_we, bit user_sel);
   begin      this.addr[31:0]  = user_addr[31:0];
      this.wdata[63:0] = user_wdata[63:0];
      this.we  = user_we;
      this.sel = user_sel;
   end  endfunction
Mô phỏng lại, bạn sẽ thấy kết quả mô phỏng sẽ không đổi so với ví dụ 7. Trong trường hợp này, sử dụng this hay không đều cho kết quả mô phỏng như nhau vì các tên biến addr, wdata, we sel được khai báo trong class là duy nhất, không trùng tên với thành phần nào khác.
Nếu thay đoạn code của constructor trong ví dụ 7 bằng đoạn code khác như sau:
Ví dụ 9 - Tên đối số của method trùng với tên biến (property) của class nhưng không dùng this
 function new (bit [31:0] addr, bit [63:0] wdata, bit we, bit sel);
   begin      addr[31:0]  = addr[31:0];
      wdata[63:0] = wdata[63:0];
      we  = we;
      sel = sel;
   end  endfunction
Trong ví dụ này, các đối số trùng tên với các biến của class. Mong muốn của người viết code là gán giá trị của các đối số truyền cho new (vế phải) cho các biến của một đối tượng class (vế trái). Tuy nhiên kết quả mô phỏng không như mong muốn.
Hình 6: Kết quả mô phỏng khi sử dụng code thay thế ở ví dụ 9
Các biến của đối tượng class không được gán giá trị được truyền vào mà vẫn giữ giá trị mặc định. Giá trị mặc định của các biến này là 0 vì các biến có kiểu bit, một kiểu dữ liệu 2 trạng thái có giá trị mặc định là 0.
Khi một thành phần chỉ được gọi bằng tên như các biến addrwdata, wesel trong function new ở ví dụ 9 thì nó sẽ ứng với khai báo trong phạm vi gần nhất. Trong trường hợp này, phạm vi gần nhất của các biến addr,  wdatawe và sel trong function new là các khai báo đối số của new. Vì vậy, cả vế phải và vế trái phép gán đều được hiểu là các đối số của function new.
Hình 7: Cách hiểu các biến ở phép gán bên trong function new của ví dụ 9
Để chỉ đúng biến nào là của đối tượng class, this sẽ được sử dụng. Thay thế đoạn code của new trong ví dụ 7 bằng đoạn code ở ví dụ sau.
Ví dụ 10 - Tên đối số của method trùng với tên biến (property) của class có dùng this
 function new (bit [31:0] addr, bit [63:0] wdata, bit we, bit sel);
   begin      this.addr[31:0]  = addr[31:0];
      this.wdata[63:0] = wdata[63:0];
      this.we  = we;
      this.sel = sel;
   end  endfunction
Kết quả mô phỏng khi sử dụng đoạn code ở ví dụ 10 hoàn toàn giống kết quả của ví dụ 7 nguyên gốc.
Hình 8: Cách hiểu các biến ở phép gán bên trong function new của ví dụ 10
Như bạn đã biết, để sử dụng một thành phần của đối tượng ở bên ngoài class thì dùng cấu trúc khai báo như sau:
<class_handle>.<member_name>
Ví dụ như wpkt.addr. Để gọi và sử dụng một thành phần của đối tượng ở bên trong class thì dùng khai báo:
this.<member_name>
Tác giả nhắc lại điều này để nhấn mạnh lại vai trò của this là một handle nhưng dùng bên trong một đối tượng class. Chúng ta hãy kiểm chứng điều này bằng cách sau:
Thứ nhất: Thêm dòng code dưới đây vào method print của ví dụ 7 để hiển thị giá trị của this
$display ("The value of this %d", this);
Thứ hai: Thêm dòng code dưới đây vào process initial, đặt dưới khai báo wpkt = new(...), để hiển thị giá trị của handle wpkt
$display ("the value of wpkt %d", wpkt);
Sau khi chạy mô phỏng, chúng ta sẽ thấy giá trị của hai handle trên là bằng nhau, nghĩa là chúng cùng chỉ đến 1 đối tượng đã tạo.
Hình 9: Kết quả kiểm tra giá trị của handle this và wpkt
Thông thường, bạn nên viết code tường minh không sử dụng this, như code của ví dụ 7. Nếu tránh sử dụng this, code sẽ có những ưu điểm sau:

  1. Code dễ đọc hiểu với các biến có vai trò khác nhau có tên khác nhau. Ví dụ, tên của đối số method khác tên biến của class.
  2. Giảm thời gian viết code.
Hai lợi điểm trên thể hiện rất rõ khi bạn viết các class có số lượng biến nhiều và số dòng code lớn.


Lịch sử cập nhật:
1) 2019.01.31 - Thêm mục "2) this"

0 bình luận:

Đăng nhận xét