[Scala] - day 9 : Classes

Ở phần trước chúng ta đã tìm hiểu về những kiểu cơ bản trong Scala và cách tạo ra các collections từ những kiểu cơ bản. Bây giờ chúng ta sẽ tự tạo ra kiểu dữ liệu mới bằng class.
Class là những core building block của ngôn ngữ hướng đối tượng, là sự kết hợp của data structure với function (“methods”). Một class được định nghĩa với value và variable có thể khởi tạo ra rất nhiều  đối tượng, mỗi đối tượng được hình thành ứng với input data. Một class có thể extend từ class khác, tạo thành 1 mối quan hệ phân cấp subclass và superclass (inheritance). Tính đa hình (polymorphism) làm cho các subclass có thể thay thế cho parent classes, trong khi đó tính bao đóng (encapsulation) cung cấp các privacy controls để quản lý những tác động bên ngoài của class.
Ok,thử định nghĩa vài class nào :
scala> class User
defined class User 
scala> val HoaiPT = new User
HoaiPT: User = User@4926097b 
scala> val isAnyRef = HoaiPT.isInstanceOf[AnyRef]
isAnyRef: Boolean = true
Chúng ta có class đầu tiên tên là User , được định nghĩa với từ khoá "class", tôi tạo 1 đối tượng mới HoaiPT từ class User và Scala trả về cho tôi 1 đối tượng từ class User cùng 1 chuỗi hexadecimal,nếu bạn tạo 1 đối tượng mới bạn sẽ thấy chuỗi này thay đổi, điều đó được hiểu mỗi đối tượng từ class bạn tạo là duy nhất.
Ta sẽ thiết kế lại class User :
scala> class User {
         | val name: String = "HoaiPT"
         | def greet: String = s"Hello from $name"
         | override def toString = s"User($name)"
         | }
defined class User 
scala> val HoaiPT = new User
HoaiPT: User = User(HoaiPT) 
scala> HoaiPT.greet
res0: String = Hello from HoaiPT
Chúng ta có class mới với value và method, nhưng class này tạo ra các đối tượng khác nhau có chung tên là "HoaiPT", tôi sẽ thay đổi đi 1 chút :
scala> class User(n: String) {
         | val name: String = n
         | def greet: String = s"Hello from $name"
         | override def toString = s"User($name)"
         |}
defined class User  
scala> val HoaiPT = new User
HoaiPT: User = User(HoaiPT) 
scala> HoaiPT.greet
res1: String = Hello from HoaiPT
Tôi dùng param n để khởi tạo name cho object, có cách viết khác hơp lý hơn :
scala> class User(val name: String) {
         | def greet: String = s"Hello from $name"
         | override def toString = s"User($name)"
         |}
defined class User
Ok,chúng ta có nói tới 3 thuộc tính của class ở phần mở đầu, thử tạo vài ví dụ để làm nó rõ ràng hơn:
scala> class A {
         | def hi = "Hello from A"
         |}
defined class A
scala> class B extends A
defined class B 
scala> class C extends B { override def hi = "hi C -> " + super.hi }
defined class C 
scala> val hiA = new A().hi
hiA: String = Hello from A 
scala> val hiB = new B().hi
hiB: String = Hello from A 
scala> val hiC = new C().hi
hiC: String = hi C -> Hello from A
Đến đây mọi thứ có lẽ khá rõ ràng bạn thấy class A có method hi và B extend A sau đó C extend B,B và C có method "hi" được kế thừa từ class parent,C override method "hi" theo cách riêng của nó.Giờ theo bạn nếu B cũng override method "hi" khi đó C gọi super.hi nó sẽ chạy như nào? bạn sẽ thử luôn bay giờ sau đó ta đi đến phần tiếp theo.
Một thuộc tính nữa gắn liền với class - polymorphism :
scala> val a: A = new A
a: A = A 
scala> val a: A = new B
a: A = B 
scala> val b: B = new A
<console>:9: error: type mismatch;
  found : A
  required: B
            val b: B = new A
                             ^ 
scala> val b: B = new B
b: B = B
Bạn thấy value kiểu A có thể khởi tạo bởi class B nhưng value kiểu B không thể khởi tạo với class A, tương tự ta có lớp "động vật"(A) và lớp "mèo"(B) extend từ lớp "động vật"(A), khi đó 1 con "mèo" là "động vật" nhưng chưa chắc "động vật" đã là mèo.

Định nghĩa 1 class

class <identifier> [type-parameters]
                             ([val|var] <identifier>: <type> = <expression>[, ... ])
                             [extends <identifier>[type-parameters](<input parameters>)]
                             [{ fields and methods }]
Bạn thấy theo công thức, bạn có thể tạo 1 class với kiểu cố định và các input tương tự như định nghĩa 1 funtion (input type và default value).
Viết lại class A, B ,C với định nghĩa đầy đủ :
scala> class A(val name: String = "HoaiPT") {
         | def hi = "Hello "+ name +" from A"
         |}
defined class A 
scala> class B[TYPE](name : TYPE) extends A
defined class B 
scala> val hiA = new A("ai do")
hiA: A = A@6a4f1a55 
scala> hiA.hi
res1: String = Helloai do from A 
scala> val hiHoaiPT = new A()
hiHoaiPT: A = A@238d68ff 
scala> hiHoaiPT.hi
res2: String = HelloHoaiPT from A 
scala> val hiB = new B("kieu String")
hiB: B[String] = B@379614be

Một vài kiểu Class

Abstract Class
Abstract class là class được thiết kế để được extend bởi class khác chứ không tự nó khởi tạo được đối tượng. Abstract class được định nghĩa bởi từ khoá "abstract", đặt trước từ khoá "class".
Một abstract class có thể được sử dụng để định nghĩa core field và method cần thiết cho các lớp con của nó mà không nhất thiết phải chỉ rõ implementation. Nhờ tính đa hình (polymorphism), một value với kiểu được khai báo ở abstract class có thể trỏ đến 1 instance của chính nó ở lớp con không trừu tượng (nonabstract), và việc thực thi methods được làm hoàn toàn ở lớp con (subclass).
Ta sẽ làm rõ lý thuyết ở ví dụ cụ thể :
scala> abstract class Car {
         | val year: Int
         | val automatic: Boolean = true
         | def color: String
         | }
defined class Car 
scala> new Car()
<console>:20: error: class Car is abstract; cannot be instantiated
              new Car()
              ^ 
scala> class RedMini(val year: Int) extends Car {
         | def color = "Red"
         | }
defined class RedMini 
scala> val m: Car = new RedMini(2005)
m: Car = RedMini@4e683bb8 
scala> class RedMini2(val year: Int) extends Car
<console>:19: error: class RedMini2 needs to be abstract, since method color in class Car of type => String is not defined
          class RedMini2(val year: Int) extends Car
                    ^ 
Bạn thấy đó 1 abstract class tự bản thân nó không thể khởi tạo và subclass từ abstract class phải đảm đủ tất cả value và method được định nghĩa ở abstract class đó.
Anonymous Class 
Nhìn tên chắc bạn cũng đoán được phần nào về kiểu class này, class không có định danh.
Làm sao để ta có thể tạo ra 1 class mà không có định danh ? phần trước chúng ta đã biết sử abstract class để xây dựng 1 chuẩn mực cho các lớp con extend từ nó, đôi khi bạn muốn sử dụng chính lớp abstract này mà không muốn tạo 1 lớp mở rộng rườm rà, bạn có thể tạo 1 lớp vô danh (anonymous class) từ chính lớp trừu tượng này (abstract class) bằng cách sau :
Ta dùng lại abstract class Car
scala> val newCar = new Car{
         | val year = 1987
         | def color = "màu đen"
         | }
newCar: Car = $anon$1@2752f6e2
Value newCar là 1 class instance , và định nghĩa của class này chính là biểu thức khởi tạo lên chính nó.Và bạn có thể thấy Scala thông báo cho ta (newCar: Car = $anon$1@2752f6e2) rằng newCar là 1 instance của class Car và là 1 anonymous ($anon) với 1 chuỗi hexadecimal ($1@2752f6e2) để biết rằng nó chỉ được tạo ra 1 lần duy nhất.
Vậy anonymous class rất phù hợp với những trường hợp bạn muốn tạo ra 1 subclass nhưng chỉ dùng có 1 lần, dùng anonymous class giúp cho code của bạn không bị mở rộng thừa thãi.

Vài kiểu Field và Method

Overloaded Methods 
Overloaded Methods cung cấp cho chúng ta 1 cơ chế gọi đến những function trùng tên nhưng khác input,trong Scala bạn có thể định nghĩa những function giống nhau nhưng khác input và tuỳ vào input mà Scala biết bạn đang gọi đến function nào.
scala> class User {
        | def funcOverLoad (s : String) = s"input is String : $s"
        | def funcOverLoad (s : Int) = s"input is Int : $s"
        | }
defined class User 
scala> val newUser = new User
newUser: User = User@20ccf40b 
scala> newUser.funcOverLoad(123)
res1: String = input is Int : 123 
scala> newUser.funcOverLoad("HoaiPT")
res2: String = input is String : HoaiPT
***) Bạn không thể define 2 function trùng tên và trùng input.
Apply Methods 
Method tên "apply" , đôi khi được coi là 1 method mặc đinh hay 1 injector method, nó có thể được gọi đến mà không cần method name.Method apply thực chất là 1 shortcut, cung cấp khả năng khởi chạy với 2 dấu ngoặc mà không cần tên method.
scala> class NhanDoi (i : Int) {
        | def apply() = i * 2
        | }
defined class NhanDoi 
scala> val nhanDoi3 = new NhanDoi(3).apply
nhanDoi3: Int = 6 
scala> val nhanDoi3 = new NhanDoi(3)()
nhanDoi3: Int = 6 
scala> val nhanDoi3 = new NhanDoi(3)
nhanDoi3: NhanDoi = NhanDoi@24105dc5 
scala> nhanDoi3()
res4: Int = 6 
scala> nhanDoi3.apply
res6: Int = 6
Lazy Values
Đúng với tên gọi của nó, lazy value không được thực thi ngay khi khai báo mà chỉ khi nào được dùng đến nó mới bắt đầu thực thi.Để khai báo 1 lazy val ta dùng từ khoá "lazy" trước từ khoá "val".

scala> class RandomPoint {
         | val x = { println("creating x"); util.Random.nextInt }
         | lazy val y = { println("now y"); util.Random.nextInt }
         |}
defined class RandomPoint 
scala> val p = new RandomPoint()
creating x
p: RandomPoint = RandomPoint@6c225adb 
scala> println(s"Location is ${p.x}, ${p.y}")
now y
Location is 2019268581, -806862774 
scala> println(s"Location is ${p.x}, ${p.y}")
Location is 2019268581, -806862774
Bạn thấy lazy val y chỉ được khởi tạo lên khi được gọi đến lần đầu tiên, và lần sau nó không cần gọi lại nữa.
Lazy val y thật sự hữu ích để đảm bảo biến đó được truy cập ít nhất 1 lần, như những biến chứa thông tin hệ thống,thông tin kết nối và những biến định nghĩa việc inject thư viện bên ngoài.
Scala Classes

Comments

  1. Chào anh, anh có thể nói rõ hơn khi tạo một instance (có dấu ngoặc và không) của một class không ví dụ với class User đầu tiên thì không có nhưng với class RandomPoint cuối cùng thì lại cần (theo em hiểu thì việc instantiate một instance sẽ theo form constructor của class - không biết e hiểu đúng không :D) cám ơn anh

    ReplyDelete

Post a Comment