F# Active Patterns

F# มี syntax พิเศษที่ช่วยให้เขียน Pattern matching ได้ง่ายขึ้น โดยใช้โครงสร้างคล้าย ๆ กับ Union ใช้วิธีประกาศ Test case ที่ต้องการในเครื่องหมาย (||) Active pattern มีหลายแบบ ขึ้นอยู่กับจำนวน Case ที่ใช้

Single-Case Active Patterns

เป็น Active pattern ที่มี Case เดียวและมี Input เพียงตัวเดียว Active pattern แบบนี้ต้อง Return ผลลัพทธ์เสมอ

1
2
3
4
5
6
7
let (|Remainder2|) x = x % 2
let checkNumber = function
| Remainder2 1 -> "even number"
| Remainder2 0 -> "odd number"

// "even number"
checkNumber 1

จากตัวอย่าง Remainder2 เป็น Pattern ที่มี Case โดยคำนวณค่า Mod จาก Input ที่ส่งเข้ามา

Partial-Case Active Pattern

เป็น Active patten ที่ Return ผลลัพท์เป็น Option และสามารถใช้ Pattern หลายตัวมาประกอบกัน เมื่อต้องการ Matching

1
2
3
4
5
6
7
8
9
10
11
let (|LessThen10|_|) x = if x < 10 then Some x else None
let (|Btw10And20|_|) x = if x >= 10 && x < 20 then Some x else None

let checkNumber x =
match x with
| LessThen10 a -> "less then 10"
| Btw10And20 a -> "between 10 and 20"
| _ -> "that's a big number"

// "that's big number"
checkNumber 20

LessThen10 และ Btw10And20 เป็น Partial pattern ที่ทดสอบว่าตัวเลขอยู่ใน Range ของตัวเองหรือไม่ สังเกตว่า Output ที่ได้จะเป็น Option ซึ่งในกรณีที่ Return ค่าเป็น None แสดงว่า Input ที่รับเข้ามาอยู่นอก Range ในกรณีที่ Input ที่รับมาไม่อยู่ใน Range ใดเลย เมื่อทำการ Matching ก็จะถูก Evaluate เข้า Case default หรือ _ นั่นเอง

Multicase Active Pattern

เป็น Pattern matching ที่มีหลาย Case โดย Pattern แบบนี้ต้อง Return choice ตามจำนวนตาม Case ที่ประกาศไว้ใน (||)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let (|Q1|Q2|Q3|Q4|) (date:System.DateTime) =
let month = date.Month
match month with
|1|2|3 -> Q1 month
|4|5|6 -> Q2 month
|7|8|9 -> Q3 month
|_ -> Q4 month

let newYearResolution date =
match date with
| Q1 _ -> "read"
| Q2 _ -> "write"
| Q3 _ -> "execute"
| Q4 _ -> "sleep"

// "sleep"
newYearResolution <| DateTime(2015,10,10)

(|Q1|Q2|Q3|Q4|) เป็น Pattern ที่ใช้หา Quarter ของ DateTime ที่รับเข้ามา สังเกตว่าฟังก์ชัน Match จะต้อง Return Q1 - Q4 ให้ครบ

Parameterized Active Pattern

เป็น Pattern ที่สามารถเพิ่ม Parameter ตัวที่สอง ซึ่งแตกต่างจาก Pattern แบบอื่นที่สามารถมี Parameter ตัวแรกเพียงตัวเดียว

1
2
3
4
5
6
7
8
9
10
let(|Divisible|_|) x y =
if y % x = 0 then Some Divisible
else None

let f2 = function
| Divisible 2 & Divisible 3 -> "divisible by 6"
| _-> "other"

// "divisible by 6"
f2 12

Divisible เป็น Pattern ที่รับ Parameter สองตัว คือ x และ y จะเห็นว่าขั้นตอนการเรียกใช้ คือ ในฟังก์ชั่น f2 มีการส่ง Parameter ตัวแรก คือ 2 และ 3 จากนั้นตอนเรียกใช้ฟังก์ชั่น f2 มีการส่ง Parameter ตัวที่สองคือ 12

เขียน F# แบบ Inline evaluation ด้วย Light Table

โปรแกรม Light Table เป็น Text editor ที่สามารถเขียนโปรแกรมแบบ Inline evaluation คือ เราสามารถพิมพ์โค้ดและสั่ง Execute คำสั่งในบรรทัดนั้น เพื่อดูผลลัพธ์ได้ทันที

ข้อดีของการเขียนโปรแกรมแบบนี้ คือ เราสามารถใช้ทดสอบโค้ดได้ง่าย ไม่ต้องใช้ IDE หรือเขียนโปรแกรมพิมพ์ผลลัพธ์ผ่าน Console ที่มีความยุ่งยาก

เครื่องมือที่ต้องใช้

  1. โปรแกรม F# interactive fsharpi (Mac) / fsi (Windows)
  2. โปรแกรม Light Table
  3. F# plugin ของ Light Table

ติดตั้ง fsharpi / fsi

ติดตั้ง Light Table

  • สามารถโหลดไฟล์ Installer จากเว็บไซต์ได้โดยตรง

ติดตั้ง F# plugin

หลังจากติดตั้ง Light Table สามารถติดตั้ง Plugin ผ่าน Plugin manager ตามขั้นตอนต่อไปนี้

  1. เปิดโปรแกรม Light Table
  2. เปิดหน้าต่าง Plugin manager โดยการกดปุ่ม Ctrl + Space พิมพ์คำว่า plugin แล้วคลิกเลือก Show plugin manager

  3. ในหน้า Plugin manager ให้ Search คำว่า F# แล้วคลิกปุ่ม Install ที่มุมด้านขวา

การใช้งาน

หนังจากติดตั้ง Plugin เราสามารถใช้งานได้ทันที่ ด้วยวิธีง่าย ๆ ดังนี้

  1. พิมพ์โคัด
  2. Hightlight บรรทัดที่ต้องการ
  3. กด Ctrl + Enter (Windows) / Command + Enterl (Mac)

จากตัวอย่าง

  • ถ้าบรรทัดที่เลือก return void โปรแกรมจะแสดงเครื่องหมายถูก
  • ส่วนบรรทัดที่ return ค่า โปรแกรมจะแสดงผลลัทธ์ที่ท้ายบรรทัด

Links

Const กับ Readonly

Constant

ค่าคงที่แบบ compile time constant ใน C# สามารถประกาศด้วย keyword const ซึ่งจะใช้ได้กับ data ประเภท primitive type ดังต่อไปนี้ เท่านั้น

1
2
Boolean, Char, Byte, SByte, Int16, UInt16,
Int32, UInt32, Int64, UInt64, Single, Double, Decimal, String

ค่าคงที่แบบ const จะไม่มีทางเปลี่ยนแปลง มันจึงถูก define เป็นส่วนหนึ่งของ type แบบ static member ไม่ใช่ instance member เหมือน field ทั่วไป

เมื่อมีการอ้างอิง const ณ ส่วนใดของโค้ด compiler จะดึงค่าที่ผูกไว้ไปฝั่งไว้ใน IL ทำให้ขณะรันโปรแกรมไม่จำเป็นต้อง allocate memory

ข้อจำกัดของ constant เพื่อเทียบกับ field

  • ไม่สามารถหาค่า address ของ constant ได้
  • ไม่สามารถส่งค่า const แบบ pass by reference ผ่าน keyword ref
  • ในกรณีที่มีการเปลี่ยนค่า const ใน assembly ที่อ้างอิง จำเป็น ต้อง compile โปรแกรมใหม่

ตัวอย่าง

1
2
3
4
5
6
7
8
9
10
using System;
public class Program {
private const string ProductName = "Visual C#";
private static string _version = "1.0";
public static void Main(String[] args) {
var name = ProductName;
var version = _version;
Console.WriteLine("{0} {1}", name, version);
}
}

จากตัวอย่าง มีการประกาศตัวแปรแบบ static ชื่อ _version และมีค่า constant ชื่อ ProductName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.method public hidebysig static
void Main (
string[] args
) cil managed
{
// Header Size: 12 bytes
// Code Size: 27 (0x1B) bytes
// LocalVarSig Token: 0x11000001 RID: 1
.maxstack 3
.entrypoint
.locals init (
[0] string,
[1] string
)

/* 0x0000025C 00 */ IL_0000: nop
/* 0x0000025D 7201000070 */ IL_0001: ldstr "Visual C#"
/* 0x00000262 0A */ IL_0006: stloc.0
/* 0x00000263 7E02000004 */ IL_0007: ldsfld string Program::_version
/* 0x00000268 0B */ IL_000C: stloc.1
/* 0x00000269 7215000070 */ IL_000D: ldstr "{0} {1}"
/* 0x0000026E 06 */ IL_0012: ldloc.0
/* 0x0000026F 07 */ IL_0013: ldloc.1
/* 0x00000270 280400000A */ IL_0014: call void [mscorlib]System.Console::WriteLine(string, object, object)
/* 0x00000275 00 */ IL_0019: nop
/* 0x00000276 2A */ IL_001A: ret
} // end of method Program::Main

เมื่อ compile เป็นคำสั่ง IL จะพบความแตกต่างระหว่างตัวแปรแบบ static ธรรมดากับค่า static const ดังนี้

  • บรรทัดที่ 17 จะเห็นว่าตำแหน่งที่อ้างถึง ค่า const มีการฝั่งค่า string literal Visual C# ไว้เลย
  • บรรทัดที่ 19 ตำแหน่งตัวแปรแบบ static ยังมีการอ้างอิงไปยัง ชื่อตัวแปรโดยใช้คำสั่ง ldsfld string Program::_version อยู่

Readonly

C# มี keyword readonly สำหรับกำหนดค่าคงที่แบบ runtime constant จะต่างจาก const คือ readonly สามารถใช้ได้กับ reference type และสามารถย้ายการกำหนดค่ามาไว้ใน constructure ได้

ตัวอย่าง

1
2
3
4
5
6
7
public class Cat {
private static readonly int Leg;
private static readonly int Eye = 2;
public Cat() {
Leg = 4
}
}

จากตัวอย่างเราสามารถกำหนดค่า constant แบบ readonly ได้ใน constructure หรือกำหนดค่าบรรทัดเดียวกับการประกาศชื่อตัวแปรก็ได้

readonly ต่างจาก const อย่างไร

  • สามารถใช้กับ reference type
  • ค่าคงที่แบบ readonly จะถูก evaluate ขณะรัน จึงไม่มีปัญหา cross-assembly versioning เหมือน const
  • readonly ไม่จำเป็นต้องเป็น static
  • แต่ละ instance ของคลาสสามารถมีค่าต่างกันได้ เพราะสามารถกำหนดค่าผ่าน constructure

Open กับ Closed Type

การใช้งาน generic ถ้าระบุ type argument ครบจำนวนที่ประกาศไว้ในเครื่องหมาย <> เราจะเรียก type นั้นว่า closed type ถ้ามีการระบุไว้เพียงบางส่วน จะเรียกว่า structured type แต่ถ้าไม่ระบุ type argument เลยจะเรียกว่า open type

ความแตกต่างระหว่าง open และ closed type

  • Open type ถือว่าเป็น type ที่ไม่สมบูรณ์ จึงไม่สามารถสร้าง instance ได้ แต่สามารถใช้เป็น input ของ operator typeof
  • Close type สามารถสร้าง instance ได้เหมือนคลาสทั่วไป

ตัวอย่าง

สร้าง GenericStruct เป็น generic type ง่าย ๆ โดยมีการระบุเงื่อนไขว่า type argument ต้องเป็น value type เท่านั้น

1
2
3
4
5
6
7
8
9
10
11
using System;
using System.Runtime.InteropServices;
using static System.Console;

public class GenericStruct<T> where T: struct {
public string Name {
get {
return typeof(T).Name;
}
}
}

ทดสอบ

ทดสอบว่าสามารถสร้าง instance ของ open และ closed type ผ่าน Activator.CreateInstance ได้หรือไม่

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Program {
static object CreateInstance(Type t) {
try {
return Activator.CreateInstance(t);
}catch(Exception e) {
WriteLine(e.Message);
return null;
}
}
public static void Main(string[] args) {
var open1 = CreateInstance(typeof(GenericStruct<>));
var close1 = CreateInstance(typeof(GenericStruct<int>));

WriteLine(open1 == null); // true
WriteLine(close1 != null); // true
WriteLine((close1 as GenericStruct<int>).Name); // Int32
}
}

จากตัวอย่างจะเห็นว่าเราไม่สามารถ สร้าง instance ของ GenericStruct<> ได้ เนื่องจากไม่ระบุ type argument ไว้ในเครื่องหมาย <> ในบรรทัดที่ 14 จึงได้ open1 มีค่า null

ส่วน closed type GenericStuct<int> สามารถสร้าง instance และแสดงค่า Name ได้ปกติ

Covariance กับ Contravariance (Draft)

ใน C# concept ที่เรียกว่า covariance และ contravariance ถูกใช้กับอะไรบ้าง

  • ใช้กับ array ในเวอร์ชัน 1.0
  • ใช้กับ delegate ในเวอร์ชั่น 2.0
  • ใช้กับ generic ในเวอร์ชั่น 4.0

Substitution

ก่อนจะรู้จักกับ covariance ต้องทำความเข้าใจกับหลักการ substitution ใน C# เสียก่อน

1
2
public class Animal { .. }
public class Cat: Animal { .. }

จาก class hierarchy จะเห็นว่า Animal เป็น supertype ของ Cat

1
2
Animal a = new Cat();    // ok
Cat b = new Animal(); // error

จากตัวอย่าง Animal เป็นเซ็ตที่ใหญ่กว่าตามหลัก substitution principle เราสามารถใช้ instance ของ Cat แทน instance ของ Animal ได้ แต่ไม่สามารถใช้ instance ของ Animal แทน Cat ให้นึกภาพว่า เราไม่สามารถนำกล่องที่ใหญ่มาใส่ในกล่องที่เล็กกว่านั่นเอง

Array

Array ที่มี element เป็น reference type ใน C# ถือว่าเป็น covariance

1
Animal[] animals = new Cat[10];

จากตัวอย่าง Animal[] compatible กับ Cat[] สามารถใช้ instance ของ Cat แทน Animal เราเรียกการ assign ค่าที่ type เล็กกว่าอยู่ด้านขวาของเครื่องหมาย = ว่า covariance

แต่ covariance ของ array เป็น covariance แบบไม่ safe เนื่องจากเราสามารถใส่ instance ของคลาสอื่นนอกเหนือจาก Cat แต่อยู่ hierarch ถัดจาก Animal ซึ่งจะทำให้เกิด ArrayTypeMismatchException ขณะรัน

1
2
// Attempted to access an element as a type incompatible with the array.
animals[0] = new Dog()

จากตัวอย่างจะเกิด error ขณะรัน เนื่องจาก backing store ของ array ถูกกำหนดให้เก็บ Cat ตั้งแต่ต้น จึงไม่สามารถเก็บ element ที่มี type อื่นได้ ถึงแม้จะเป็น subtype ของ Animal เหมือนกันก็ตาม

Covariance ใน array ถูก implement ตั้งแต่ C# 1.0 เพื่อให้เหมือนกับ Java ในขณะนั้น ถึงปัจจุบันปัญหา TypeMissMatch ก็ยังอยู่ ไม่สามารถตรวจสอบขณะ compile ได้

ใน C# รุ่นต่อมา มีการ implement covariance กับส่วนอื่น ซึ่งจะไม่มีปัญหาลักษณะนี้ เนื่องจากมีการออกแบบให้มีความปลอดภัยตั้งแต่ต้น

Method group

Covariance

1
2
Cat MakeCat() { ... }
Func<Animal> makeAnimal = MakeCat

Covariant จะทำให้ Func<Animal> compatible กับ MakeCat เนื่องจาก Animal ใหญ่กว่า Cat จำสามารถทำ implicit conversion

Contravaraince

ตัวอย่างนี้มีสอง method คือ ShowCat ที่รับ Cat เป็น parameter และ Animal ที่รับ Animal เป็น parameter

1
2
void ShowCat(Cat c) {}
void ShowAnimal(Animal c) {}

จากตัวอย่างเราสามารถ assign method ไปยัง delegate Action ได้ดังนี้

1
2
Action<Cat> action2 = ShowAnimal;   // legal
Action<Animal> action1 = ShowCat; // illegal

จากหลัก substution principle Cat นั้นเล็กว่า Animal

  • บรรทัดแรก Action<Cat> compatible กับ ShowAnimal เนื่องจาก เราสามารถใช้ Cat แทน Animal ได้ ในตัวอย่างนี้ถึงแม้จะใช้หลัก substitution เหมือนเดิม แต่ทิศทางการ assign ค่าด้วยเครื่องหมาย = จะกลับข้างกัน เราเรียก operation ลักษณะนี้ว่า contravariance
  • บรรทัดที่สอง Action<Animal> ไม่สามารถใช้แทน ShowCat ได้ เนื่องจากจะผิดหลัก substitution เพราะว่า Animal ใหญ่กว่า Cat จึงไม่สามารถใช้ Animal แทน Cat นั่นเอง

Generic delegate

ใน C# 4.0 มีสิ่งที่เรียกว่า safe generic variant ที่ใช้ได้กับ interface และ delegate

1
public delegate TResult Func<in T, out TResult>(T arg);

จากตัวอย่าง เราสามารถประกาศ delegate ชื่อ Func ที่มี type parameter ตัวแรกเป็น contravariance T และมี type parameter ตัวที่สองเป็น covariance TResult

  • ใช้ keyword in เพื่อระบุว่า type parameter เป็น contravariance
  • ใช้ keyword out เพื่อระบุว่า type parameter เป็น variance

เรียกใช้งาน

1
2
Func<Animal, Cat> f1 = (x) => new Cat();
Func<Cat,Animal> f2 = f1;

Func<Animl,Cat> compatible กับ Func<Cat, Animal> เนื่องจากจาก type parameter ตัวแรกเป็น contravariance Cat ซึ่งเป็น type ที่เล็กกว่าจึงสามารถส่งเข้าไปในฟังก์ชั่นแทน Animal ส่วน type parameter ตัวที่สองเป็น covariance เราจึงใช้ Animal เพื่อรับค่าแทน Cat ที่ return มาจากฟังกํชั่นได้เช่นเดียวกัน

Links

Checked กับ Unchecked

Max & Min

Primitive type ใน C# เช่น short float หรือ int จะมี min และ max value เป็นค่าต่ำสุดและสูงสุดที่เป็นไปได้ ค่านี้ถูกกำหนดโดยจำนวน bit ของ type เช่น int มี 32 bit เลขฐานสองขนาด 32 bit สามารถแสดงตัวเลขได้ถึง 4294967296 แต่เนื่องจาก int เป็น sign integer ต้องใช้ 1 bit สำหรับเก็บ flag + หรือ - ดังนั้น bit ที่ใช้สำหรับเก็บตัวเลขจึงเหลือ 31 bit ทำให้สามารถเก็บค่าต่ำสุดและสูงสุดดังนี้

1
2
public const Int32 MaxValue = 2147483647;
public const Int32 MinValue = -2147483648;

จากปัจจัยนี้ทำให้ int ไม่สามารถใช้เก็บค่าที่ต่ำหรือสูงกว่า min และ max นี้ได้ ถ้าพยายาม assign ค่านอกจาก range นี้ก็จะเกิดสิ่งที่เรียกกว่า overflow

ภาษาต่าง ๆ จะมีวิธีจัดการกับ overflow ที่ต่างกัน เช่น C, C++ จะไม่สนใจ overflow และไม่ถือว่า overflow คือ error ดังนั้นโปรแกรมเมอร์ ต้องควบคุมจัดการเอง

ภาษา Visual Basic จะตรวจจับ overflow ขณะทำงาน และจะ throw exception ออกมาเมื่อเจอเคสแบบนี้

ส่วน C# โดย default จะปิดการตรวจจับ overflow ไว้ คือ unchecked นั่นเอง แต่สามารถเปิดให้ checked ได้ด้วยการ compile ด้วย flag /checked+ ซึ่ง compiler จะเพิ่มโค้ดชุดพิเศษแทนที่ชุดปกติ เช่น เปลี่ยน IL instruction จาก add เป็น add.ovf

ทดสอบ

มาดูกันว่าโปรแกรม Test.cs ที่จะใช้ทดสอบเมื่อ compile ด้วย flag /checked+ โปรแกรมที่ได้นั้นต่างกับโปรแกรมปกติอย่างไร

1
2
3
4
5
6
7
using System;
public class Test {
public static void Main(String[] args) {
var i = Int32.MaxValue + Int32.Parse("1");
Console.WriteLine(i);
}
}

ลอง compile Test.cs เป็นสองแบบ คือ แบบธรรมดา และแบบมี /checked+

  1. Compile Test.cs เป็น exe ก่อนด้วย dmcs
  2. Decompile exe ที่ได้จากข้อ 1 ด้วย monodis เพื่อเช็ค IL instruction

Compile และ decompile แบบธรรมดา

1
2
dmcs -out:Unchecked.exe Unchecked.cs
monodis --output=Unchecked.il Unchecked.exe

Compile ด้วย /checked+

1
2
dmcs /checked+ -out:Checked.exe Checked.cs
monodis --output=Checked.il Checked.exe

IL instruction

IL instruction ของโปรแกรมต่างกันอย่างไร

แบบที่ 1 Unchecked.il ได้จาก Unchecked.exe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// method line 2
.method public static hidebysig
default void Main (string[] args) cil managed
{

// Method begins at RVA 0x2058
.entrypoint
// Code size 24 (0x18)
.maxstack 2
.locals init (
int32 V_0)

IL_0000: ldc.i4 2147483647
IL_0005: ldstr "1"
IL_000a: call int32 int32::Parse(string)
IL_000f: add
IL_0010: stloc.0
IL_0011: ldloc.0
IL_0012: call void class [mscorlib]System.Console::WriteLine(int32)
IL_0017: ret
} // end of method Test::Main

แบบที่ 2 Checked.il ได้จาก Checked.exe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// method line 2
.method public static hidebysig
default void Main (string[] args) cil managed
{

// Method begins at RVA 0x2058
.entrypoint
// Code size 24 (0x18)
.maxstack 2
.locals init (
int32 V_0)

IL_0000: ldc.i4 2147483647
IL_0005: ldstr "1"
IL_000a: call int32 int32::Parse(string)
IL_000f: add.ovf
IL_0010: stloc.0
IL_0011: ldloc.0
IL_0012: call void class [mscorlib]System.Console::WriteLine(int32)
IL_0017: ret
} // end of method Test::Main

จะเห็นว่า IL instruction ของทั้งสองโปรแกรมมีหน้าแทบเหมือนกัน ยกเว้นเพียงบรรทัดที่ 14 ที่มี instruction ต่างกัน คือ add กับ add.ovf

การทำงาน

จาก IL ที่ได้จะเห็นว่า มีเพียง instruction เดียวที่ต่างกัน แล้วโปรแกรมทั้งสองจะทำงานต่างกันหรือไม่

1. รันโปรแกรมแบบ unchecked

รันโปรแกรมได้ปกติ ไม่เกิด exception แต่ได้ผลลัพท์ คือ -2147483648 ซึ่งผิดคาดจากที่ตั้งใจไว้ ตาม sense การบวกเลขเราน่าจะได้ผลลัพท์ลักษณะนี้ 2147483647 + 1 = 21474836478

1
2
$ mono Unchecked.exe
-2147483648

2. โปรแกรมแบบ checked

จะมี error คือ System.OverflowException ซึ่งหมายความว่า CLR ตรวจเจอ overflow จึงทำการ throw exception ออกมา

1
2
3
4
5
6
7
$ mono Checked.exe

Unhandled Exception:
System.OverflowException: Arithmetic operation resulted in an overflow.
at Test.Main (System.String[] args) in <filename unknown>:line 0
[ERROR] FATAL UNHANDLED EXCEPTION: System.OverflowException: Arithmetic operation resulted in an overflow.
at Test.Main (System.String[] args) in <filename unknown>:line 0

สรุป

จากการทดสอบจะเห็นว่า โปรแกรมที่ไม่เช็ค overflow สามารถทำงานได้ โดยไม่มี error แต่ผลลัพท์ไม่ถูกต้อง ในการใช้งานจริงโปรแกรมเมอร์ต้องแน่ใจว่า โค้ดที่เขียนจะต้องอยู่ภายใน range ของ type นั้น มิฉะนั้นโปรแกรมก็จะทำงานผิดพลาด

ส่วนโปรแกรมที่เช็ค overflow ซึ่งโดยปกติจะมีประสิทธิภาพต่ำกว่าแบบ unchecked เล็กน้อย จะมี runtime exception และไม่แสดงผลลัพท์ใด ๆ ในทางปฏิบัติโปรแกรมเมอร์สามารถจัดการ exception ที่เกิดขึ้นโดยใช้ประโยค try ... catch จากนั้นก็เขียนโลจิกเพิ่มเติมเพื่อกัดการกับ error เพื่อช่วยป้องกันความผิดพลาดของโปรแกรม

สรุปสั้น ๆ คือ

  • unchecked เร็วกว่า
  • checked ปลอดภัยกว่า

หมายเหตุ

นอกจากการใช้ compiler flag /checked+ ซึ่งจะมีผลกับ instruction ของทั้งโปรแกรม เราสามารถเขียนโค้ดสำหรับ checked หรือ unchecked overflow เฉพาะส่วนที่ต้องการได้ โดยใช้ operator checked กับ unchecked ตามตัวอย่างนี้

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
public class Test {
public void Checked1() {
Int32 i = Int32.MaxValue;
i = checked (i + 1);
}
public void Checked2() {
checked {
Int32 i = Int32.MaxValue;
i = i + 1;
}
}
public void Unchecked() {
unchecked {
Int32 i = Int32.MaxValue;
i = i + 1;
}
}
}

String กับ string

ใน C# จะมี alias ที่ match กับ type ใน base class library (BCL) เป็น keyword ตัวพิมพ์เล็ก เช่น System.String มี alias คือ string ทั้ง String ใหญ่และ string เล็กในทางเทคนิกแล้วสามารถใช้แทนกันได้

Alias ของ C# มีทั้งหมด 15 ตัว โดย map อยู่กับ BCL type ดังนี้

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object:  System.Object
string: System.String
bool: System.Boolean
byte: System.Byte
sbyte: System.SByte
short: System.Int16
ushort: System.UInt16
int: System.Int32
uint: System.UInt32
long: System.Int64
ulong: System.UInt64
float: System.Single
double: System.Double
decimal: System.Decimal
char: System.Char

Alias ทุกตัว คือ keyword ของภาษา

1
2
3
4
5
6
7
8
abstract as base bool break byte case catch char checked class const
continue decimal default delegate do double else enum event explicit
extern false finally fixed float for foreach goto if implicit in
in (generic modifier) int interface internal is lock long namespace
new null object operator out out (generic modifier) override params
private protected public readonly ref return sbyte sealed short sizeof
stackalloc static string struct switch this throw true try typeof uint
ulong unchecked unsafe ushort using virtual void volatile while

เมื่อ alias กับ BCL type ทำงานเหมือนกัน แล้วจะเลือกใช้ตัวไหน? เรื่องนี้มีความเห็นหลายแบบ เช่น

ใน C# coding style ของทีม corefx บอกว่าพวกเขาใช้ alias ทั้งหมดทุกกรณี เหตุผลอย่างหนึ่งคือ alias เป็น keyword ประโยชน์ที่ได้จากมันก็คือ สามารถเรียกใช้เมื่อไหร่ก็ได้ โดยไม่ต้องเปิด namespace ใด ๆ แม้ได้ System

บางคนให้ความเห็นว่า engineer ของ Microsoft สร้าง alias เพื่อให้คล้ายกับ C++ เพื่อให้โปรแกรมเมอร์เข้าใจได้ง่าย เป็นแนวคิดเก่า ตอนนี้ก็ควรกลับมาใช้ BCL type ซึ่งเป็น design เริ่มต้นของ C# เสียที [1]

ทำไมถึงควรใช้ BCL type ก็เนื่องจาก Api บางตัวของ .Net ใช้ชื่อเดียวกับ BCL type เช่น Convert.ToSingle ฟังก์ชันนี้ return ค่าเป็น Single (float) สามารถเขียนได้สองแบบ คือ

1
2
float value = Convert.ToSingle("12.0")
Single value = Convert.ToSingle("12.0")

แบบแรก ต้องการ convert เป็น Single แต่กลับ return ค่าออกมาเป็น float
แบบที่สอง จะดูตรงไปตรงมา คือ convert เป็น Single และรับค่าด้วย Single

เรื่องนี้เกี่ยวกับ readability ถึงแม้โค้ดทั้งสองแบบจะทำงานเหมือนกัน แต่แบบที่สองจะเข้าใจได้ง่ายกว่า แม้กับคนที่ไม่เคยเขียน C# ก็ตาม

ความเห็นอีกแบบคือ ควรใช้ alias เป็นหลัก จะใช้ BCL type ก็ต่อเมื่อมีการอ้างถึงสมาชิกในคลาสเท่านั้น เพราะไม่ make sense ถ้าจะเรียกใช้ method จาก keyword ควรเรียกจาก class มากกว่า

จากความเห็นข้างต้น สามารถสรุป style การใช้ได้ 3 แบบ คือ

แบบที่ 1 ใช้ BCL ทั้งหมด

1
2
3
4
String text;
String.IsNullOrEmpty(text);
Double number;
Double.TryParse(text, out number);

แบบที่ 2 ใช้ alias ทั้งหมด

1
2
3
4
string text;
string.IsNullOrEmpty(text);
double number;
double.TryParse(text, out number);

แบบที่ 3 ใช้ alias ในประโยคประกาศตัวแปร และใช้ BCL เมื่อมีการเรียกใช้ method ใน class

1
2
3
4
string text;
String.IsNullOrEmpty(text);
double number;
Double.TryParse(text, out number);

Links

  1. To String or to string.
  2. What’s the difference between String and string?.
  3. Language or BCL data types.