Skip to content

บทที่ 2 — Variables + Types (value vs reference, nullable, var)

← บทที่ 1 | สารบัญ | บทที่ 3: Control Flow →

C# เป็น statically typed — รู้ชนิดข้อมูล (type) ของทุกตัวแปรตั้งแต่ตอน compile ไม่ต้องรอรันจริง

C# ยังเป็น strongly typed — เข้มงวดเรื่องชนิดข้อมูล ใส่ค่าผิด type ให้ตัวแปรไม่ได้เลย เช่นเอา string ไปใส่ตัวแปรที่ประกาศเป็น int จะ error ตอน compile ทันที ไม่ต้องรอไปพังตอนรัน — พฤติกรรมสองอย่างนี้เหมือนกับ Java/TypeScript

csharp
// statically typed — compiler รู้ type ตั้งแต่ตอน compile ไม่ใช่ตอนรัน
int age = 30;        // type = int
// age = "hello";   // ❌ error ตอน compile — เปลี่ยน type ไม่ได้

// strongly typed — ใส่ค่าผิด type ไม่ได้
string name = "Alice";
// name = 42;       // ❌ error: int ≠ string

1. ประกาศตัวแปร — 3 รูปแบบ

csharp
// 1. ระบุ type ตรง ๆ (explicit type)
int age = 30;
string name = "Alice";

// 2. var — type inference (compiler เดา type จากค่าที่ใส่ให้)
var count = 10;          // เป็น int
var greeting = "hello";  // เป็น string
var users = new List<string>(); // เป็น List<string> = รายการของ string ที่เพิ่ม/ลบได้ (<string> บอกว่าเก็บ string เท่านั้น — generic อธิบายเต็มบท 7)

// 3. const — ค่าคงที่ที่รู้ตั้งแต่ตอน compile
const double Pi = 3.14159;

readonly — กำหนดค่าตอนรัน ใส่ค่าได้ครั้งเดียวใน constructor (= โค้ดที่รันตอนสร้าง object ครั้งแรก — เรียนเต็มบท 5) ตัวอย่างใช้กับ class — syntax class/public/constructor จะอธิบายเต็มในบท 5 ตอนนี้แค่ดูภาพรวม:

csharp
// ตัวอย่างไฟล์แยก — เก็บไว้ดูตอนเรียนบท 5
public class Config
{
    public readonly string AppName;
    public Config() { AppName = "MyApp"; }  // () { } นี้คือ constructor — โค้ดที่รันตอนสร้าง object ครั้งแรก เรียนเต็มบท 5
}

💡 ใช้ var เมื่อ type ดูได้ชัดเจน — var users = new List<User>(); ดีกว่า List<User> users = new List<User>(); — แต่อย่าใช้ตอนที่ type ไม่ชัด (var x = GetData(); — อ่านยาก)


2. Built-in Types

C# ต่างจาก Python ตรงที่ตัวแปรบางชนิด "เก็บค่าตรง ๆ" ส่วนบางชนิด "เก็บที่อยู่" ของข้อมูล ความต่างนี้สำคัญเพราะมันกำหนดว่าตอน copy ตัวแปร จะได้ก้อนข้อมูลใหม่แยกกัน หรือได้แค่ตัวชี้ไปที่ก้อนเดิม:

  • value type = ชนิดที่เก็บค่าตรง ๆ — ตอน assign จะ copy ค่าทั้งก้อน (เรียกว่า copy semantics) เช่น int, bool, struct
  • reference type = ชนิดที่เก็บที่อยู่ชี้ไปยังข้อมูล — ตอน assign จะ copy แค่ตัวชี้ (pointer) ไม่ copy ข้อมูลจริง (เรียกว่า reference semantics) เช่น string, class

stack vs heap (ภาพคร่าว ๆ)

  • stack = พื้นที่สำหรับตัวแปรท้องถิ่น (local) — อายุสั้น พอ method จบก็เคลียร์ทันที
  • heap = พื้นที่สำหรับ object ที่ต้องอยู่นาน — มี GC (Garbage Collector — ตัวเก็บกวาดอัตโนมัติ) ตามเก็บของที่ไม่มีใครใช้แล้ว

⚠️ หมายเหตุ: value type ไม่ได้อยู่ใน stack เสมอไป — ถ้าเป็น field ของ class มันอยู่ใน heap ด้วย สิ่งที่สำคัญกว่าคือ "copy semantics" ไม่ใช่ตำแหน่งที่เก็บ

ความต่างนี้ส่งผลต่อพฤติกรรมตอนคัดลอก (copy) และเปรียบเทียบ:

Value types (เก็บค่าตรง ๆ ใน stack)

จำนวนเต็ม (integer):

Typeช่วงค่าหมายเหตุ
sbyte-128..1278-bit, มีค่าติดลบ
byte0..2558-bit, ไม่มีค่าติดลบ
short-32,768..32,76716-bit
int-2³¹..2³¹-132-bit — ใช้บ่อยที่สุด
long-2⁶³..2⁶³-164-bit
ushort, uint, ulong0..2ⁿ-1unsigned (ไม่มีค่าติดลบ)

จำนวนทศนิยม (floating point):

Typeขนาดหมายเหตุ
float32-bitเติม suffix f ท้ายค่า: 1.5f
double64-bitdefault ของค่าทศนิยม
decimal128-bitแม่นยำ (ใช้กับเงิน) — เติม suffix m: 1.5m

ตัวอักษร + ค่าจริง/เท็จ:

Typeหมายเหตุ
char1 ตัวอักษร (UTF-16) — 'A'
booltrue/false

struct ที่มีมาให้ในตัว (ของ .NET):

  • DateTime, TimeSpan — วันเวลา
  • DateOnly, TimeOnly — แค่วันที่/แค่เวลา (.NET 6+) ใช้แทน DateTime เมื่อไม่สนใจอีกฝั่ง
  • DateTimeOffset — วันเวลาพร้อม timezone offset — แนะนำใช้แทน DateTime สำหรับช่วงเวลาแน่นอน (DateTime กำกวมเรื่อง timezone) — สำหรับ timestamp เช่น CreatedAt ใน DB ใช้ DateTimeOffset.UtcNow เป็น default ที่ปลอดภัยที่สุด
  • Guid — รหัสไม่ซ้ำ 128-bit
  • Int128, UInt128 — จำนวนเต็ม 128-bit (.NET 7+)
  • Half — float 16-bit (.NET 5+)

🛈 เนื้อหาขั้นสูงnint/nuint (native int, ขนาดตามเครื่อง 32/64-bit) มีไว้ใช้ตอน interop ระดับต่ำ มือใหม่ข้ามไปก่อนได้

กฎเรื่องเงิน (Money rule):

csharp
// ❌ ผิด — float/double มี rounding error (ค่าเพี้ยนจากการปัดเศษ)
double priceBad = 0.1 + 0.2;       // 0.30000000000000004

// ✅ ใช้ decimal สำหรับเงิน
decimal priceGood = 0.1m + 0.2m;   // 0.3 เป๊ะ

ตั้งชื่อตัวแปรต่างกัน (priceBad / priceGood) เพื่อไม่ให้ compiler บ่นว่าประกาศซ้ำ — ในโค้ดจริงเลือกใช้ decimal ตัวเดียวก็พอ

Reference types (เก็บข้อมูลใน heap, ใน stack เก็บแค่ pointer คือตัวชี้ที่อยู่)

csharp
string text = "hello";       // string = reference ที่แก้ไม่ได้ (immutable)
object o = 42;               // เก็บ int ใน object ได้ (อธิบายเต็มในหัวข้อ 7 ของบทนี้)
int[] arr = { 1, 2, 3 };     // array (อาเรย์) — `{ ... }` คือการกำหนดค่าเริ่มต้น (รายละเอียดบท 7)
List<int> list = new();      // List<T> = รายการของหลายค่า; new() = ย่อของ new List<int>() (compiler เดาเอง)

💡 อย่ายึดติด syntax 2 บรรทัดล่างint[] { ... } กับ List<int> = new() ใช้ feature ที่ยังไม่สอน (collection initializer + generic + target-typed new) ตอนนี้แค่อ่านเข้าใจว่ามันเป็น "reference type" ก็พอ — รายละเอียดอยู่บท 7


3. Value Type vs Reference Type — เรื่องสำคัญที่สุด

Value type (struct, enum, primitive)

  • เก็บค่าตรง ๆ
  • assign = copy
  • pass to method = copy

Reference type (class, array, string, interface, delegate)

  • เก็บ "ที่อยู่" ของ object
  • assign = copy reference (ชี้ object เดียวกัน)
  • pass to method = copy reference
csharp
// value type
int a = 5;
int b = a;        // คัดลอกค่า
b = 10;
Console.WriteLine(a);  // 5 — ไม่กระทบ a

// reference type — List<int> = รายการของ int (generic อธิบายเต็มบท 7)
// new() { 1, 2, 3 } = สร้าง List ใหม่และใส่ค่า 1,2,3 เริ่มต้น (syntax เรียนเต็มบท 7)
List<int> x = new() { 1, 2, 3 };
List<int> y = x;  // ก๊อป reference (ที่อยู่) — ชี้ของก้อนเดียวกัน
y.Add(4);
Console.WriteLine(x.Count);  // 4 — กระทบ! เพราะชี้ list เดียวกัน

struct vs class

syntax struct / class / public (access modifier) เรียนเต็มในบท 5 — ตอนนี้ดูภาพรวมว่า struct ทำตัวเหมือนค่า, class ทำตัวเหมือนตัวชี้

csharp
// struct = value type
public struct Point
{
    public int X;
    public int Y;
}

// class = reference type
public class Person
{
    public string Name = "";
    public int Age;
}
csharp
// { X = 1, Y = 2 } คือ object initializer — ตั้งค่า field ตอนสร้าง object เลย ไม่ต้องเขียน constructor เอง (เรียนเต็มบท 5)
// new() แบบไม่มีชื่อ type = ย่อของ new Point() เพราะ compiler เดา type จากด้านซ้าย — รายละเอียดบท 5
Point p1 = new() { X = 1, Y = 2 };
Point p2 = p1;       // ก๊อปทั้งก้อน
p2.X = 99;
Console.WriteLine(p1.X);  // 1

Person a = new() { Name = "Alice" };
Person b = a;        // ก๊อป reference (ชี้ object เดียวกัน)
b.Name = "Bob";
Console.WriteLine(a.Name);  // Bob

💡 หมายเหตุ (C# 10+) — มีวิธีเขียน struct ที่ immutable สั้นกว่านี้สำหรับข้อมูลที่ไม่เปลี่ยนค่า — เรียนในบท 10 เมื่อถึงเรื่อง record

กฎ:

  • struct = ข้อมูลเล็ก, immutable, lifetime สั้น (Point, Color, decimal)
  • class = ส่วนใหญ่ของ domain object

4. String — เป็น reference type แต่ behave เหมือน value

csharp
string a = "hello";
string b = a;
b = "world";        // ไม่กระทบ a เพราะ string แก้ไม่ได้ (immutable)
Console.WriteLine(a);  // hello

String immutable (string แก้ไม่ได้) = ทุก operation (การกระทำ) สร้าง string ใหม่ — s + "x" สร้าง object ใหม่เสมอ

  • ต่อ string จำนวนน้อย/รู้จำนวนแน่นอน → + หรือ $"..." (interpolation) ก็ดีพอ
  • ต่อ string ในลูปจำนวนมากหรือไม่รู้จำนวน → ใช้ StringBuilder (ตัวช่วยต่อ string ที่ไม่สร้างใหม่ทุกครั้ง)
csharp
using System.Text;   // StringBuilder อยู่ใน namespace นี้
                     // (System.Text ไม่อยู่ใน implicit usings — ต้อง using ทุกครั้ง)

var sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.Append(i);
string result = sb.ToString();

5. Nullable Reference Types (NRT) — feature สำคัญที่สุด

C# 8+ เพิ่ม Nullable Reference Types (NRT) — ใช้กับ reference type (เช่น string, class) ที่เรียนไปในหัวข้อ 2 (ไม่ใช่ value type เช่น int/bool) compiler จะตรวจว่าตัวแปร reference จะเป็น null ได้ไหม

💡 warning ≠ error — ต่างกันนะ: เคสด้านล่างที่ขึ้น "warning" คือ compiler เตือน ว่าเสี่ยง แต่ ยังคอมไพล์ผ่านและรันได้ (ไม่ใช่ error ที่หยุดการคอมไพล์) — มันเตือนเพื่อให้เราแก้ก่อนเจอ NullReferenceException ตอนรันจริง อย่ามองข้าม warning พวกนี้

csharp
// เปิดฟีเจอร์นี้ใน .csproj — ถ้าใช้ dotnet new console เวอร์ชันล่าสุดจะเปิดให้แล้ว
// ถ้ายังไม่เปิด เพิ่ม <Nullable>enable</Nullable> ใน <PropertyGroup> ของ csproj เอง

string name = null;          // ❌ warning: เอา null ใส่ตัวแปรที่ห้ามเป็น null
string? maybeName = null;    // ✅ ประกาศชัดว่าเป็น null ได้ (ใส่ ? ท้าย type)

int len = name.Length;       // ❌ warning + ถ้ารันจริงจะโยน NullReferenceException
                             //    (compiler เตือนแล้ว — อย่าเพิกเฉย)

if (maybeName != null)
{
    int len2 = maybeName.Length;  // ✅ compiler วิเคราะห์เส้นทางโค้ดแล้วรู้ว่าตรงนี้ไม่ null
    Console.WriteLine(len2);
}
// หมายเหตุ: ถ้าไม่มี { } ครอบ บรรทัดประกาศตัวแปร (int len2 = ...) จะ compile ไม่ผ่าน
// เพราะกฎ C#: declaration ไม่สามารถเป็น "embedded statement" ของ if ได้ (ต้องใส่ { } เสมอ)

ผลลัพธ์: หลีกเลี่ยง NullReferenceException (error ที่เกิดเมื่อใช้ค่า null ผิด) ได้เกือบหมดตั้งแต่ตอน compile

Operators ที่ใช้กับ null

csharp
string? name = Console.ReadLine(); // Console.ReadLine() คืน string? (อาจ null ถ้า stdin ปิด)

// 1. null-coalescing ?? (เลือกค่าสำรองเมื่อเป็น null)
string display = name ?? "anonymous";

// 2. null-conditional ?. (เรียกต่อเฉพาะเมื่อไม่ null)
int? len = name?.Length;  // ถ้า name=null → ได้ null (ไม่ระเบิด)

// 3. null-coalescing assignment ??= (ใส่ค่าให้เฉพาะตอนเป็น null)
name ??= "default";  // ถ้า name เป็น null ค่อยใส่ค่าให้

// 4. null-forgiving ! (บอก compiler ว่า "มั่นใจว่าไม่ null")
string sure = name!;  // ใช้เมื่อรู้แน่ ๆ — ระวัง! ถ้าผิดจะระเบิดตอนรัน

Nullable value types (value type ที่เป็น null ได้ — มีมาก่อนแล้ว)

Value type ใช้ ? มาตั้งแต่ C# 2:

csharp
int? maybeAge = null;
int? age = 30;

if (age.HasValue)          // HasValue = มีค่าหรือไม่
    Console.WriteLine(age.Value);  // Value = ค่าจริงข้างใน

int actual = age ?? 0;

สรุปสั้น ๆ ว่า int? คืออะไร:

  • int? = "int ที่อาจเป็น null ได้"
  • มี property 2 ตัวสำคัญ: HasValue (มีค่าไหม) และ Value (ค่าจริง)
  • ชื่อเต็มของมันคือ Nullable<int> — ตัว <int> เรียก generic syntax ซึ่งเรียนเต็มในบท 7 ตอนนี้แค่ใช้ int? แบบสั้นพอ

6. Type Conversion

การแปลง type ใน C# มี 2 แบบ: implicit (อัตโนมัติ เกิดเมื่อไม่เสียข้อมูล เช่น int→long) และ explicit/cast (ต้องเขียนสั่งเอง เกิดเมื่ออาจเสียข้อมูล เช่น double→int)

นอกจากนี้ยังมีการแปลงระหว่าง string กับตัวเลข ซึ่งใช้ Parse/TryParse แทน:

csharp
// implicit (อัตโนมัติ — ไม่เสียข้อมูล)
int i = 10;
long l = i;          // ได้
double d = i;        // ได้

// explicit cast (สั่งแปลงเอง — อาจเสียข้อมูล)
double d2 = 3.7;
int i2 = (int)d2;    // 3 (ตัดเศษทิ้ง = truncate)

// แปลง string เป็นตัวเลข (parse)
int n = int.Parse("42");
double dbl = double.Parse("3.14");

// TryParse — คืน bool บอกว่าแปลงสำเร็จหรือไม่ และส่ง int ออกมาผ่าน out
bool ok = int.TryParse("42", out int parsed);
if (ok)
{
    Console.WriteLine($"OK: {parsed}");  // ✅ ใช้ parsed ได้เฉพาะเมื่อ ok=true
}
else
{
    Console.WriteLine("แปลงไม่ได้");
    // ❌ ห้าม: ใช้ parsed เมื่อ ok=false — ค่าใน parsed ไม่มีความหมาย (spec ไม่การันตีว่าจะคืน 0)
}

// แปลงเป็น string / ชนิดอื่น
string s = i.ToString();
string s2 = i.ToString("000");  // "010"

// Convert.ToInt32(double) มีพฤติกรรมต่างจาก (int) cast ที่ควรรู้:
//  - Convert.ToInt32(3.7) คืน 4 (ปัดเศษปกติ) ← ต่างจาก (int)3.7 = 3 ซึ่งตัดเศษทิ้ง
//    * กรณี midpoint (จุดกึ่งกลางพอดี) เช่น 2.5 → Convert ใช้ banker's rounding คืน 2 (ปัดเป็นเลขคู่ — กฎนี้ใช้เฉพาะตอนเลข .5 เป๊ะเท่านั้น)

// Convert.ToInt32(string) เป็นคนละ overload กัน — พฤติกรรมต่างจาก int.Parse:
//  - Convert.ToInt32((string)null) คืน 0 (ไม่ throw) ← ต่างจาก int.Parse(null) ที่ throw
//  - string ว่างหรือแปลงไม่ได้ → ยัง throw FormatException เหมือน int.Parse
//  - ค่าเกิน int → OverflowException (เหมือนกันทั้งสอง overload)
int n2 = Convert.ToInt32("42");

💡 TryParse ดีกว่า Parse เกือบทุกครั้ง — ไม่ throw exception (ไม่โยน error ทำโปรแกรมหยุด) ถ้าแปลงไม่ได้


7. Boxing / Unboxing — กับดัก performance

🛈 หัวข้อขั้นสูง — มือใหม่อ่านผ่านตาได้ก่อน กลับมาอ่านซ้ำเมื่อเจอปัญหา performance จริง ๆ กฎจำง่าย: "ใช้ List<T> เสมอ ไม่ใช้ ArrayList" ก็เพียงพอในตอนนี้

เมื่อ value type (เช่น int) ถูกใส่ใน object มันจะถูก "box" (boxing = ห่อค่าขึ้น heap) — ก๊อปขึ้น heap (จองหน่วยความจำ) และ "unbox" (unboxing = แกะค่ากลับ) ตอนแกะกลับ ถ้าเกิดบ่อยใน loop จะกระทบ performance เพราะสร้างขยะให้ GC (Garbage Collector — ตัวเก็บกวาดหน่วยความจำ) ต้องตามเก็บ:

csharp
int i = 42;
object o = i;       // boxing: ก๊อป int → heap (allocate = จองหน่วยความจำ!)
int i2 = (int)o;    // unboxing: ก๊อปจาก heap → stack

ใน hot loop (= ลูปที่ทำงานบ่อยมากหรือวนหลายล้านรอบ — คำว่า "hot" ในศัพท์ performance หมายถึง "ถูกเรียกบ่อยจนเป็น bottleneck") boxing = bug ด้าน performance ตัวใหญ่

csharp
using System.Collections;   // ArrayList อยู่ใน namespace นี้ (ไม่อยู่ใน implicit usings)

// ❌ เกิด boxing ทุกรอบ
// ArrayList = collection แบบเก่าก่อนมี generic (.NET 1.x) — ไม่ต้องจำ ใช้ List<T> เสมอ
ArrayList list = new();
for (int i = 0; i < 1_000_000; i++)
    list.Add(i);  // boxing!

// ✅ generic — ไม่เกิด boxing
List<int> list2 = new();
for (int i = 0; i < 1_000_000; i++)
    list2.Add(i);

กฎ: ใช้ generic เสมอ — List<T> (เดี๋ยวบท 7 อธิบาย generic เต็ม) ArrayList เป็น legacy ที่ไม่ควรใช้ในโค้ดใหม่อีกเลย


8. var vs dynamic vs object

csharp
var x = 10;           // compiler รู้ว่าเป็น int — type ตายตัวตั้งแต่ compile
object y = 10;        // type = object — ต้อง cast (สั่งแปลง) ก่อนใช้
dynamic z = 10;       // รู้ type ตอน runtime — compiler ไม่ตรวจ

x = "hello";          // ❌ error: ประกาศเป็น int ไปแล้ว
y = "hello";          // ✅ ได้ แต่เรียก method ของ string ตรง ๆ ไม่ได้
z = "hello";          // ✅ ได้ และ z.Length เรียกได้ (ตัดสินตอน runtime)

dynamic เป็น keyword ของ C# (ไม่ต้อง using เพิ่ม) — ใช้ตอน interop (เชื่อมต่อ) กับ COM (Component Object Model = เทคโนโลยีเก่า Windows เช่น Excel/Word automation), Python, JSON ที่ไม่รู้ schema (โครงสร้างข้อมูล) ล่วงหน้า — อย่าใช้พล่อย ๆ ในโค้ดทั่วไป (ช้า + compiler ตรวจสอบ type ไม่ได้ + ไม่มี IntelliSense คือตัวช่วยเติมโค้ดอัตโนมัติ)


9. Enum

enum ใช้สร้าง "ชุดค่าที่กำหนดไว้" ที่มีชื่อสื่อความหมาย (แทนตัวเลขดิบ) — เช่น สถานะผู้ใช้ ทำให้โค้ดอ่านง่ายและกันใส่ค่าผิด แต่ละสมาชิกได้ค่าตัวเลขเริ่มจาก 0 อัตโนมัติ:

public = access modifier (ตัวกำหนดสิทธิ์เห็นโค้ด) — ให้ส่วนอื่นเห็นได้ บท 5 อธิบายเต็ม

csharp
public enum Status
{
    Active,      // = 0
    Suspended,   // = 1
    Deleted      // = 2
}

Status s = Status.Active;
int n = (int)s;             // ต้องสั่ง cast (แปลง) เอง
Status s2 = (Status)1;      // → Suspended

// กำหนดค่าตัวเลขเอง
public enum HttpStatus
{
    Ok = 200,
    NotFound = 404,
    Internal = 500
}

// flags (ใช้รวมหลายค่าด้วย bit) — เนื้อหาขั้นสูง ข้ามได้ถ้ายังใหม่
// [Flags] = attribute (metadata ที่เขียนในวงเล็บเหลี่ยม [...]
//            เพื่อบอกข้อมูลพิเศษให้ compiler/runtime — รายละเอียดเรียนในบทหลัง)
//            ในที่นี้บอก .NET ว่า enum นี้ใช้รวมหลายค่าด้วย bit ได้
// 1 << n = "เลื่อน bit ซ้าย n ตำแหน่ง" → 1<<0=1, 1<<1=2, 1<<2=4 (กำลัง 2)
// | = bitwise OR (รวม bit ของหลายค่าเข้าด้วยกัน)
[Flags]
public enum Permission
{
    None  = 0,
    Read  = 1 << 0,  // 1
    Write = 1 << 1,  // 2
    Exec  = 1 << 2,  // 4
    All   = Read | Write | Exec  // = 7 (มีครบทั้ง 3 bit)
}

var p = Permission.Read | Permission.Write;  // มีทั้ง Read และ Write
bool canRead = p.HasFlag(Permission.Read);   // true

10. Tuples

csharp
// แบบใหม่ (C# 7+)
(string name, int age) = ("Alice", 30);    // deconstruction = แยกค่าออกเป็นตัวแปร
var person = (Name: "Alice", Age: 30);     // ตั้งชื่อช่อง (named)
Console.WriteLine(person.Name);

// ใช้ตอน method คืนค่า
// (string, int) = ชนิด tuple ที่มี 2 ค่า (string + int)
// => คือ expression-bodied method — ย่อของ { return (...); } (บท 4 อธิบาย)
(string, int) GetPerson() => ("Alice", 30);

var (n, a) = GetPerson();

// ละค่าที่ไม่ใช้ด้วย _ (ทิ้งช่องนั้นไป)
var (_, ageOnly) = GetPerson();

ดีสำหรับ "คืนหลายค่า" โดยไม่ต้องสร้าง class


11. Constants — const vs readonly vs static readonly

C# มีหลายวิธีประกาศค่าคงที่ที่ต่างกันละเอียด — const (ค่าตายตัวตั้งแต่ compile), readonly (กำหนดได้ครั้งเดียวตอน runtime/constructor), static readonly (ค่าคงที่ระดับ class) เลือกใช้ให้ถูกตามว่าค่ารู้ตอนไหน:

csharp
public class Config
{
    public const double Pi = 3.14159;            // รู้ค่าตั้งแต่ compile
    public readonly string Name;                  // ระดับ instance, ใส่ค่าใน ctor (constructor)
    public static readonly DateTime Start = DateTime.Now;  // ระดับ class (type-level)

    public Config(string name) { Name = name; }
}

const ต้องเป็นค่าที่รู้ตั้งแต่ compile (literal คือค่าที่เขียนตรง ๆ) — เก็บใน metadata (ข้อมูลกำกับ), ไม่กินหน่วยความจำของ object


12. Common Pitfalls

  1. var x = null; — compiler ไม่รู้ type → error → ต้องระบุ string? x = null;
  2. double price = 0.1 + 0.2; ใช้กับเงิน → ใช้ decimal
  3. string == "x" คือ value compare (ต่างจาก Java!) — ไม่ต้อง .Equals() ในเคสปกติ ใช้ string.Equals(a, b, StringComparison.OrdinalIgnoreCase) สำหรับ case-insensitive
    • ✅ เช็ค null + ว่าง: ใช้ string.IsNullOrEmpty(s) หรือ string.IsNullOrWhiteSpace(s) ซึ่ง null-safe และอ่านง่ายกว่า s == null || s == ""
    • ⚠️ กับดัก: ถ้าตัวแปรเป็น object ไม่ใช่ string เช่น object x = "hi"; object y = "hi"; x == y → ผลลัพธ์จะเป็น reference equality (เทียบที่อยู่ ไม่ใช่เทียบค่า) ทั้งที่ทั้งคู่เป็น string เหมือนกัน สาเหตุคือ compiler เลือก operator == จาก type ที่ประกาศไว้ (คือ object) ตั้งแต่ตอน compile ไม่ได้ดูว่าข้างในเป็น string — ต้องระวังตรงนี้เป็นพิเศษ
  4. (int)null — error (value type ไม่รับ null)
  5. int.Parse("abc") → FormatException → ใช้ TryParse
  6. เก็บเงินด้วย double → rounding ใน production = bug ใหญ่
  7. ใช้ ArrayList — ของเก่า legacy, boxing → ใช้ List<T>

🛠️ Checkpoint 2 — ลงมือทำ

  1. สร้างโปรเจกต์ที่รับ 2 จำนวนจาก stdin → คำนวณ + − × ÷ → print
  2. ใช้ decimal สำหรับ "เงิน" + format {x:C} (currency)
  3. ถ้า input ไม่ใช่ตัวเลข → ใช้ TryParse + แจ้ง error สวย ๆ
  4. สร้าง struct Money ที่มี field decimal Amount + string Currency และ class Account ที่มี field string Owner + Money Balance (รูปแบบ field ดูตัวอย่าง §3 — primary constructor ของ struct/class จะเรียนในบท 5/10)
  5. assign account ไปตัวแปรใหม่ → เปลี่ยน Balance → ดูว่ากระทบไหม (เพราะ class ↔ struct)

สรุปบทที่ 2

  • C# = statically typed — type ทุกตัวแปร
  • value type (struct) vs reference type (class) — เรื่องสำคัญ
  • ใช้ decimal สำหรับเงิน, int สำหรับ general, double สำหรับ scientific
  • เปิด NRT (Nullable Reference Types) — string? vs string
  • ใช้ var เมื่อ type ชัดเจน
  • ParseTryParse เสมอเมื่อ input อาจผิด

→ บทที่ 3: Control Flow