Home » Featured

Delphi 2009 Generic

31 กรกฎาคม 2010 One Comment

หนึ่งใน feature ที่น่าสนใจที่สุดที่มีมากับ Delphi 2009 ก็คือเรื่อง Generic เพราะนี่ถือว่าเป็นการ update ภาษาของตัว Object Pascal เองหลังจากที่ไม่มีใหม่ๆ อะไรเพิ่มเติมมาซะนาน ซึ่ง Generic จะช่วยให้โปรแกรมเมอร์สามารถทำ polymorphism ได้ยืดหยุ่นมากขึ้นนอกเหนือไปจากการทำ override และ overload method ที่จริง Generic นั้นมีมานานแล้วในภาษา C++ ต่อมาภาษา Java ก็ support ใน version 1.5 และ C# ก็ support ใน version 2.0 เช่นกัน เราลองมาดูครับว่ามันเป็นยังไง และมีประโยชน์ยังไง

The land before Generic

สมมุติว่าเรามี class การ sort แล้วกัน โดยปกติถ้าไม่ได้ใช้ dataset เก็บข้อมูลเป็นตาราง เราก็ต้องใช้ array หรือ dynamic array เก็บค่าต่างๆ ตัวอย่าง class ก็น่าจะประมาณนี้

TMySorted = class
private
FElements : array of integer;  // เก็บ integer ไว้ใน array
FCount : integer;  // เก็บจำนวนของ element
procedure FSort;  // method ในการ sort จะถูก call จาก
AddElement ทุกครั้งที่มีการ add element ใหม่

public
procedure AddElement(e : integer);  // add element ใหม่เข้าไป
// จะไปลงตรงไหนไม่รู้
// เพราะจะถูก sort เก็บไว้

function GetElement(position : integer = -1) : integer;
// เอา element ตำแหน่ง position
// ออกมา ถ้าไม่ใส่คือเอาตัวแรกสุด

end;

สังเกตุว่า class นี้จะรองรับ element เฉพาะ integer เท่านั้น ถ้าต้องการให้รับ type อื่นๆ ด้วย ก็ต้องมี class TMySorted เฉพาะ type นั้นๆ ไป เช่น TMySortedReal, TMySortedString, etc. ทำให้เกิด code ซ้ำๆ กัน เพราะใช้ algorithm แบบเดียวกันหมด ถ้าต้องการเปลี่ยน algorithm ก็ต้องแก้ code ทุก class เลยเพราะแต่ละ class มี code ที่ซ้ำๆ กันอยู่

เราอาจจะแก้ปัญหาด้วยแนวคิดของ polymorphism ก็ได้ คือแทนที่จะกำหนด type เป็น integer ก็กำหนด type เป็น TObject แทน เพราะทุก type ถูก inherit มาจาก TObject อยู่แล้ว แต่ TObject ก็ไม่ support native type เพราะ Delphi เป็นภาษาที่ต่อยอดมาจาก Pascal ซึ่ง integer, real, etc. เป็น

ตัวอย่าง class ในกรณีที่ใช้ TObject

TMySortedObject = class
private
FElements : array of TObject;  // เป็น object แทน integer
FCount : integer;
procedure FSort;
public
procedure AddElement(e : TObject);  // add object แทน integer
function GetElement(position : integer = -1) : TObject;
//return object แทน
end;

เวลาเรียกใช้ก็

var  mySortedObj : TMySortedObject;
person:  TPerson;  // เปลี่ยนจาก interger
// มาเก็บ object ของ class TPerson แทน
begin
mySortedObj := TMySortedObject.Create();

person :=  TPerson.Create;
mySortedObj.AddElement( person as TObject );
// ทำการ type cast บังคับให้เป็น object แทน
person  := (mySortedObj.GetElement(0) as TPerson);
// บังคับ object ที่ return ออกมาเป็น integer อีกที
end;

การ casting object แบบนี้เรียกว่า late binding คือไปเชื่อม type กันตอน runtime ซึ่งตอน compile (design time) ไม่มีปัญหาอะไร แต่จะเกิดปัญหาตอน runtime เพราะเป็นไปได้ที่คนอื่นเอา class นี้ไปใช้แล้วอาจเกิด add object ของ TPerson แต่ตอน get กลับ cast เป็น TAnimal หรืออาจจะ add เข้าไปหลายๆ type แต่ get ออกมา ก็ต้องใช้ is operator ทดสอบว่า เป็น string ไหม ก็ cast string, แต่ถ้าเป็น TPerson ก็ต้อง cast ให้เป็น TPerson อีก ซึ่งทำให้การ test ทำได้ยาก และ code จะซับซ้อนเกินไปด้วย (มี if เยอะ)

Generic

คราวนี้เรามาดูว่า ถ้าเอา Generic มาใช้ เราจะเขียนยังไง ตัวอย่าง class …

TMySortedGeneric <T> = class
// กำหนด T ขึ้นมาเป็น anonymous type ก่อน
// เมื่อ T ถูก reference ที่ไหนใน class
// จะถือว่าเป็น typeเดียวกัน

private
FElements : array of T;  // ให้ array เก็บ element ของ T
FCount : integer;
procedure FSort;
public
procedure AddElement(e : T);
// add type T (ขึ้นอยู่กับว่าตอนเอาไปใช้ T จะเป็นอะไร)
function GetElement(position : integer) : T;
// return object ของ type T ออกมา

end;

หมายเหตุ สำหรับใครที่ยังไม่รู้ ในส่วน body ของ code ให้ใช้ Delphi ช่วยสร้าง body ของ code ให้นะครับ ไม่ต้องเขียนเอง โดย click ไปที่ method ที่ยังไม่ได้เขียน code แล้วกด ctrl + shift + c บน keyboard ครับ

เวลาเรียกใช้ก็

var  mySorted : TMySortedGeneric <string>;
// เอา string ไปแทนค่า T
// เพราะฉะนั้นใน T ใน class ก็จะเป็น string หมด
s : string;
begin
s := ‘Test1′;
mySorted := TMySortedGeneric <string>.Create;
mySorted.AddElement(s);  // add ได้ตรงๆ
s := mySorted.GetElement(0); // get ได้เลย ไม่ต้อง cast
mySorted.AddElement( 20 );
// compile ไม่ผ่าน เพราะ compiler
// จะเช็ค type ตอน compile เลย

end;

นั่นคือ class TMySortedGeneric จะสามารถรองรับ type อะไรก็ได้ ขึ้นอยู่กับการนำไปใช้งาน เช่น

mySorted := TMySortedGeneric <int>.Create;
mySorted := TMySortedGeneric <real>.Create;

เป็นต้น

ในกรณีที่เรามี anonymous type มากกว่า 1 type เราสามารถประกาศได้ แบบนี้

TMySortedGeneric<T1, T2> = class

end;

จากตัวอย่างที่ผ่านมา เป็นการกำหนดให้ class นั้นเป็น Generic class ในกรณีที่เราต้องการให้ class ของเรา support Generic โดยที่ไม่ได้กำหนดทั้ง class ก็ได้ เราก็เขียน method นั้นให้ support Generic แทน เช่น

TMySortedGeneric = class

Public
Function AddItem<T>(item: T): integer;
// Generic method โดยที่ Type T จะมี scope อยู่ใน
// method AddItem เท่านั้น

end;

Generic and Polymorphism

บางครั้งเราต้องการเขียน Generic class หรือ method ให้รับ type ที่อยู่ใน type กลุ่มเดียวเท่านั้น เช่น

TPerson = class
End;

TEmployee = class(TPerson)
End;

TManager = class(TManager)
End;

ซึ่ง TPerson เป็น class แม่, TEmployee inherit มาจาก TPerson และ TManager inherit มาจาก TEmployee และ class TClaculateBonus เป็น class ที่เก็บ business logic ในการคำนวณโบนัสของพนักงาน

TCalculateBonus< TPerson > = class
End;

การเขียน code แบบนี้ เรากำหนด class ที่จะให้เป็น generic class เป็น TPerson ไปเลย หมายความว่า TCalculateBonus จะยอมรับ object ของ class TPerson หรือ class ที่ inherit มาจาก TPerson เท่านั้น ตามกฏการทำ polymorphism ของ OOP ลองดูตัวอย่าง code ครับ

var  cal: TCalculateBonus<TPerson>;
// ประกาศตัวแปรชื่อ cal เป็น type
// TCalculateBonus รับเฉพาะ TPerson
p: TPerson;  // ตัวแปรที่จะเอามา test
e: TEmployee;
m: TManager;
begin
cal := TCalculateBonus<TPerson>.Create;
// <–สร้าง object cal ขึ้นมา
p := TPerson.Create;
e := TEmployee.Create;
m := TManager.Create;
cal.Calculate(p);              // รับ p เข้าไป ไม่มีปัญหา
// เพราะ p เป็น TPerson
cal.Calculate(e);              // รับ e ได้ เพราะ e เป็น TEmployee
// ซึ่ง inherit มาจาก TPerson อีกที
cal.Calculate(m);            // รับ m ได้ เพราะ m เป็น TManager
//ซึ่ง inherit มาจาก TEmployee และ TPerdon
cal.Calculate(cal);           // *** ERROR เพราะ cal ไม่ได้เป็น
// class ที่อยู่ในตระกูลเดียวกับ TPerson
end;

หรือเราอาจประกาศแบบนี้ก็ได้

TCalculateBonus<T: TPerson> = class
// T เป็น anonymous class ซึ่งเป็น type ของ
// TPerson (T “is a” TPerson)
Public
procedure Calculate(p: T);   // กำหนด type เป็น T แทน
end;

คิดว่าคงจะพอเห็นภาพนะครับ ด้วยวิธีของ Generic จะช่วยให้เราเขียน code ได้สะอาดขึ้น เพราะเราสามารถ reuse code ที่ซ้ำๆ กันได้ดีกว่า รวมทั้งยังปลอดภัยขึ้น มี bug น้อยลงด้วย เพราะถ้า type ที่เรากำหนดไม่เข้ากฏเกณฑ์ที่กำหนดไว้ compiler จะฟ้อง error ตอน compile เลย ไม่ต้องไปรอ error ตอน runtime ครับ

One Comment »

  • pop said:

    ถ้าใช้เดลไฟรุ่นใหม่ก็น่าสนใจมากเลยครับ
    แต่คงติดที่ยังต้องใช้ของเก่าซึ่งทำไม่ได้นั่นเอง
    จากที่เห็นมือเซียนทั้งหลายก็ยังใช้ d7 กันอยู่เลย
    เพราะติดที่คอมโปเนนท์ยาวเป็นหางว่าวเลยขยับไปไม่ได้

Leave your response!

Add your comment below, or trackback from your own site. You can also subscribe to these comments via RSS.

Be nice. Keep it clean. Stay on topic. No spam.

You can use these tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

This is a Gravatar-enabled weblog. To get your own globally-recognized-avatar, please register at Gravatar.