Lớp trong Objective-C

Lớp trong Objective-C có 2 phần:

  • Interface: chứa phần khai báo lớp. Interface trong Objective-C không giống khái niệm interface trong C#, Java hoặc C++. Nó chỉ đơn thuần là phần khai báo cho lớp. Objective-C có một khái niệm tương ứng với interface trong C#, Java, C++ gọi là protocol. Interface được chứa trong file header ClassName.h.
  • Implementation: chứa phần định nghĩa cho lớp, nằm trong file ClassName.m.

Ví dụ: giả sử bạn muốn tạo lớp LoaiNguoi như sau:

lop-trong-objective-c-6

Bạn làm các bước sau trong XCode (giả sử bạn đã tạo project):

Bước 1: từ project đang mở, bấm menu File > New > File…

lop-trong-objective-c-1

Bước 2: cửa sổ chọn template hiện ra, tùy thuộc loại project bạn đã tạo (iOS hay OS X), chọn mục Source, rồi chọn Cocoa Class (tùy thuộc phiên bản XCode, có thể ghi là Objective-C class)

lop-trong-objective-c-2

Bước 3: cửa sổ tiếp theo cho phép bạn đặt tên lớp, hãy ghi tên lớp LoaiNguoi vào mục Class, Subclass of bạn để là NSObject, Language chọn Objective-C (mình viết bài này lúc đã có ngôn ngữ Swift nên mới có mục chọn này, nếu bạn dùng XCode phiên bản cũ hơn thì không có mục chọn Language).

lop-trong-objective-c-3

Sau khi bấm Next, để ý bên vùng navigator area, bạn sẽ thấy xuất hiện 2 file như hình:

lop-trong-objective-c-4

Ngoài ra, bạn cũng có thể tự tạo riêng lẻ 2 file: Header File cho phần interface và Objective-C File cho phần implementation:

lop-trong-objective-c-5

Interface

Interface được khai báo trong file .h. Cú pháp khai báo như sau:

@interface ClassName : NSObject {
   // khai báo biến
}
   // khai báo phương thức
@end

Trong đó:

  • ClassName: là tên lớp do bạn tự xác định. Quy ước đặt tên lớp:
    • Tên lớp không trùng nhau trong dự án, và không trùng với các lớp thư viện có tham chiếu.
    • Chỉ chứa ký tự a-z, bao gồm chữ hoa thường, ký tự số và _.
    • Tên không bắt đầu bằng ký tự số.
    • Objective-C không có khái niệm tương ứng với package trong Java hay namespace như C#, C++. Lớp trong Objective-C không thể được nhóm thành gói, do đó, khả năng trùng tên cao. Để giải quyết, người ta quy ước đặt tiền tố (prefix) cho lớp. Ví dụ NSWindow có tiền tố NS (viết tắt của NeXTSTEP) là các lớp cơ sở. Khi đặt tên lớp bạn cũng nên đặt một tiền tố nào đó cho nó.
  • : NSObject: NSObject là lớp cha cho mọi lớp trong Objective-C. Dấu : ở đầu nghĩa là kế thừa, tức bạn đang cho lớp của mình kế thừa NSObject. Trừ khi bạn muốn lớp được tạo kế thừa một lớp khác, nếu không hãy cho nó kế thừa lớp NSObject.

Implementation

Implementation được định nghĩa trong file .m, và nó phải tương ứng với file .h, ví dụ file .h bạn đã khai báo bao nhiêu phương thức thì trong file .m bạn phải định nghĩa bấy nhiêu. Cú pháp định nghĩa trong file .m như sau:

#import “ClassName.h”

@implementation ClassName
   // định nghĩa phương thức
@end

Các biến

Các biến được khai báo trong phần // khai báo biến của interface. Biến không có định nghĩa tương ứng bên phần implementation.

Các biến này được gọi là instance variable (biến của thể hiện của lớp). Instance variable là biến của đối tượng sinh ra từ lớp. Chúng giữ giá trị khác nhau ở những đối tượng khác nhau. Ví dụ: tên có thể được xem là biến instance của lớp Loài người, vì tên của mỗi người là riêng biệt nhau.

Nếu bạn đã biết Java/C++/C#, bạn sẽ biết về biến của lớp (class variable). Trong Objective-C, các biến khai báo trong lớp là các biến instance, và không có khái niệm class variable. Bù lại, vì là con của C, Objective-C cũng có từ khóa static, nhưng nó làm việc theo kiểu của C.

Cú pháp khai báo biến instance:

[@phạm_vi]   kiểu_dữ_liệu   tên_biến;

Trong đó:

  • @phạm_vi: phạm vi truy xuất của biến, bao gồm:
    • private: biến chỉ có thể truy xuất trong lớp
    • public: biến có thể truy xuất ngoài lớp
    • protected: biến chỉ có thể truy xuất ở trong lớp và lớp con của nó, đây là mặc định, nếu bạn không ghi rõ

Ví dụ:

@interface LoaiNguoi : NSObject {
   @protected NSString *_ten;
   @private int _tuoi;
}

Phương thức (method)

Phương thức được khai báo trong phần // khai báo phương thức của interface. Bạn sau đó, phải định nghĩa phương thức trong phần // định nghĩa phương thức của implementation

Cú pháp:

-|+(kiểu_trả_về)tên_phương_thức

Ý nghĩa:

  • : instance method, khi gọi, bạn phải gọi thông qua đối tượng
  • +: class method, khi gọi, bạn gọi thông qua lớp

Ví dụ: khai báo phương thức instance method DiHoc():

@interface LoaiNguoi : NSObject {
   NSString *_ten;
   int _tuoi;
}

-(void)DiHoc;

@end

Tham số

Trong ví dụ trên, chúng ta đang khai báo phương thức không có tham số. Nếu phương thức có tham số, ta sử dụng cú pháp sau khi khai báo phương thức:

-|+(kiểu_trả_về)tên_phương_thức:tham_số_1   tham_số_2

Cú pháp tham số:

(kiểu_dữ_liệu) tên_biến_tham_số     tên_tham_số_2:(kiểu_dữ_liệu) tên_biến_tham_số_2 …

Ví dụ: giả sử ta cần khai báo thêm phương thức ganTen() để gán tên cho đối tượng lớp LoaiNguoi. Phương thức này sẽ nhận một tham số vào là tên được gán cho đối tượng, lúc đó, ta thêm câu lệnh khai báo sau vào trước @end của interface:

-(void)ganTen:(NSString*)ten;

Nếu ta có nhiều tham số, cách khai báo hơi phức tạp hơn chút. Ví dụ, câu lệnh dưới đây khai báo phương thức ganTen(), nhưng sẽ nhận vào cả tên lẫn tuổi cho đối tượng:

-(void)ganTen:(NSString*)ten vaTuoi:(int)tuoi;

Cú pháp khai báo phương thức có nhiều tham số trong Objective-C lạ và khó hiểu so với các ngôn ngữ khác, nhưng thật ra, nếu đọc theo kiểu của con người, nó lại dễ hiểu hơn. Ví dụ câu mã trên nếu đọc ra là gán tên x và tuổi y, giả sử truyền lần lượt Nguyen Van A20 cho 2 tham số, ta sẽ đọc thành:

gán tên Nguyen Van A và tuổi 20

Sẽ dể hiểu hơn so với cách đọc ở các ngôn ngữ khác. Ví dụ giả sử nếu phương thức trên được viết theo kiểu C, nó được viết thành:

ganTenVaTuoi(char *ten, int tuoi)

Như vậy sẽ đọc là: gán tên và tuổi (x, y).

Định nghĩa lớp

Giả sử bạn đã khai báo lớp LoaiNguoi ở file interface như sau:

@interface LoaiNguoi : NSObject {
   NSString *_ten;
   int _tuoi;
}

-(void)DiHoc;
-(void)ganTen:(NSString*)ten vaTuoi:(int)tuoi;

@end

Lúc đó, ở file LoaiNguoi.m, bạn sẽ phải định nghĩa 2 phương thức này, như sau:

#import "LoaiNguoi.h"

@implementation LoaiNguoi

-(void)DiHoc {
   // viết mã cho thân phương thức ở đây
}

-(void)ganTen:(NSString*)ten vaTuoi:(int)tuoi {
   // viết mã cho thân phương thức ở đây
}

@end

Tạo đối tượng

Để tạo đối tượng, bạn dùng cú pháp:

Tên_lớp  *tên_biến_đối_tượng;

Ví dụ: để tạo biến đối tượng tên ngVanA từ lớp LoaiNguoi, ta dùng lệnh:

LoaiNguoi *ngVanA;

Câu lệnh trên chỉ khai báo, chứ chưa khởi tạo đối tượng. ngVanA lúc này vẫn đang là một con trỏ NULL, chưa trỏ đến bất kỳ vùng nhớ nào. Để có thể sử dụng, bạn phải cấp phát và khởi tạo cho biến:

  • Cấp phát: cấp phát vùng bộ nhớ để lưu thông tin và thuộc tính đối tượng
  • Khởi tạo: gán các giá trị ban đầu cho các thành phần của đối tượng

Để cấp phát cho biến đối tượng, ta cần gọi phương thức alloc. Để khởi tạo chúng, ta gọi phương thức init. Hai phương thức này có được do kế thừa từ NSObject. Đó là lý do bạn phải cho lớp kế thừa NSObject. alloc là phương thức class method, tức là ta cần gọi thông qua lớp chứ không phải đối tượng. init là phương thức của instance, khi gọi alloc thì alloc đã trả về cho bạn instance mới tạo.

Tuy nhiên, việc khởi tạo bằng cách gọi init là không cần thiết. Thường khi cấp phát, các thành phần cũng đã được đặt giá trị ban đầu. Bạn chỉ cần gọi alloc là đủ, gọi init chỉ là tùy chọn. Nó chỉ hữu dụng khi bạn muốn đối tượng khởi tạo theo ý mình, khi đó, bạn ghi đè (override) phương thức init rồi tự xác định cách khởi tạo các thành phần cho đối tượng.

Ngoài cách dùng allocinit, bạn cũng có thể gọi new. new sẽ tự động gọi allocinit.

Gọi phương thức

Để gọi phương thức của lớp hoặc đối tượng, ta dùng cú pháp:

[lớp_hoặc_đối_tượng   phương_thức];

Nếu phương thức có tham số:

[lớp_hoặc_đối_tượng   phương_thức:giá_trị_1   tên_tham_số_2:giá_trị_2 …];

Ví dụ, gọi phương thức alloc để cấp phát cho đối tượng:

LoaiNguoi *ngVanA = [LoaiNguoi alloc];

Nếu bạn muốn vừa cấp phát, vừa khởi tạo, bạn có thể gọi phương thức lồng nhau:

LoaiNguoi *ngVanA = [[LoaiNguoi alloc] init];

Thông điệp

Phương thức trong Objective-C phải gọi chính xác hơn là thông điệp (message), vì Objective-C theo mô hình message. Khi gọi phương thức của đối tượng, bạn đang gởi thông điệp cho đối tượng. Đối tượng sẽ nhận thông điệp, và được gọi là receiver, tên thông điệp được gọi là selector:

lop-trong-objective-c-7

Về mặt lý thuyết, chúng khác nhau, vì khi truyền thông điệp, đối tượng có thể chọn đáp ứng lại thông điệp hoặc không, hoặc gởi lại thông điệp cho đối tượng khác,… Hệ quả của việc này dẫn đến việc thông điệp có thể được gởi tới bất kỳ đối tượng nào, cho dù đối tượng đó có thể có định nghĩa thông điệp đó hoặc không. Nếu gởi thông điệp chưa định nghĩa cho đối tượng, nó chỉ trả về nil chứ không báo lỗi:

NSString *emailAddress;
NSString *testString = [emailAddress addSparkles];
// testString = nil vì NSString không có phương thức addSaprkles

Ngoài ra, như trong ví dụ trên, emailAddress chỉ mới khai báo, tức nó vẫn đang là con trỏ NULL, nhưng bạn vẫn có thể gởi thông điệp cho nó mà không gặp lỗi nào, và trường hợp này, nó sẽ trả về nil. Bạn không cần phải kiểm tra đối tượng đã khởi tạo chưa trước khi gởi thông điệp cho nó như đối với các ngôn ngữ lập trình khác, như thế này:

NSString *emailAddress;

if (emailAddress != nil) {
   NSString *testString = [emailAddress addSparkles];
}

Lưu ý: các trình biên dịch mới gần đây bổ sung tính năng ARC (Automatic Reference Counting), đã hạn chế bớt việc gởi thông điệp không được định nghĩa cho đối tượng, như vậy, XCode có thể báo lỗi khi bạn cố biên dịch chương trình có dòng mã như ví dụ ở trên:

NSString *testString = [emailAddress addSparkles];

Phương thức getter, setter

Thông thường, để đảm bảo tính đóng gói của lập trình hướng đối tượng, các biến instance thường được khai báo private hoặc protected. Khi đó, từ ngoài lớp, ta không thể thao tác trực tiếp với các biến này. Để gán giá trị cho biến này, ta tạo một phương thức gán gọi là setter. Ngược lại, để lấy giá trị từ biến ra, ta tạo một phương thức gọi là getter. Nếu biến chỉ cho phép ghi chứ không đọc, ta chỉ việc bỏ phần định nghĩa getter, và nếu biến chỉ cho phép đọc chứ không ghi, ta bỏ bớt định nghĩa setter.

Ví dụ: với biến _tuoi trong lớp LoaiNguoi, ta khai báo và định nghĩa hai phương thức sau:

Bổ sung vào phần khai báo phương thức ở file LoaiNguoi.h:

-(void)setTuoi:(int)tuoi;

-(int)getTuoi;

Bổ sung vào phần định nghĩa của file LoaiNguoi.m:

-(void)setTuoi:(int)tuoi{

_tuoi = tuoi;

}

-(int)getTuoi{

return _tuoi;

}

Chạy thử

Kết hợp toàn bộ từ đầu bài, bạn thử hoàn chỉnh các mã sau và chạy thử:

File LoaiNguoi.h:

#import <Foundation/Foundation.h>

@interface LoaiNguoi : NSObject {
   NSString *_ten;
   int _tuoi;
}

-(void)DiHoc;
-(void)ganTen:(NSString*)ten vaTuoi:(int)tuoi;
-(void)setTuoi:(int)tuoi;
-(int)getTuoi;
-(void)setTen:(NSString*)ten;
-(NSString*)getTen;

@end
#import "LoaiNguoi.h"

@implementation LoaiNguoi

-(void)DiHoc {
   NSLog(@"%@ dang di hoc", _ten);
}

-(void)ganTen:(NSString*)ten vaTuoi:(int)tuoi {
   _ten = ten;
   _tuoi = tuoi;
}

-(void)setTuoi:(int)tuoi{
   _tuoi = tuoi;
}

-(int)getTuoi{
   return _tuoi;
}

-(void)setTen:(NSString*)ten{
   _ten = ten;
}

-(NSString*)getTen{
   return _ten;
}

@end

Trở lại file main.m của project, trong hàm main(), ta viết mã như sau:

#import <Foundation/Foundation.h>

#import "LoaiNguoi.h"

int main(int argc, const char * argv[]) {

   @autoreleasepool {

      LoaiNguoi *ngVanA = [[LoaiNguoi alloc] init];
      [ngVanA ganTen:@"Nguyen Van A" vaTuoi:20];
      [ngVanA DiHoc];

      LoaiNguoi *ngVanB = [LoaiNguoi new];
      [ngVanB setTen:@"Nguyen Van B"];
      [ngVanB DiHoc];
   }

   return 0;

}

Nguồn tham khảo:

3 Comments

Bình luận

Bình luận Facebook

lời bình luận