โหมดมืด
บทที่ 2 — Variables + Types (value vs reference, nullable, var)
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 ≠ string1. ประกาศตัวแปร — 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..127 | 8-bit, มีค่าติดลบ |
byte | 0..255 | 8-bit, ไม่มีค่าติดลบ |
short | -32,768..32,767 | 16-bit |
int | -2³¹..2³¹-1 | 32-bit — ใช้บ่อยที่สุด |
long | -2⁶³..2⁶³-1 | 64-bit |
ushort, uint, ulong | 0..2ⁿ-1 | unsigned (ไม่มีค่าติดลบ) |
จำนวนทศนิยม (floating point):
| Type | ขนาด | หมายเหตุ |
|---|---|---|
float | 32-bit | เติม suffix f ท้ายค่า: 1.5f |
double | 64-bit | default ของค่าทศนิยม |
decimal | 128-bit | แม่นยำ (ใช้กับเงิน) — เติม suffix m: 1.5m |
ตัวอักษร + ค่าจริง/เท็จ:
| Type | หมายเหตุ |
|---|---|
char | 1 ตัวอักษร (UTF-16) — 'A' |
bool | true/false |
struct ที่มีมาให้ในตัว (ของ .NET):
DateTime,TimeSpan— วันเวลาDateOnly,TimeOnly— แค่วันที่/แค่เวลา (.NET 6+) ใช้แทน DateTime เมื่อไม่สนใจอีกฝั่งDateTimeOffset— วันเวลาพร้อม timezone offset — แนะนำใช้แทนDateTimeสำหรับช่วงเวลาแน่นอน (DateTime กำกวมเรื่อง timezone) — สำหรับ timestamp เช่น CreatedAt ใน DB ใช้DateTimeOffset.UtcNowเป็น default ที่ปลอดภัยที่สุดGuid— รหัสไม่ซ้ำ 128-bitInt128,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); // helloString 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); // true10. 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
var x = null;— compiler ไม่รู้ type → error → ต้องระบุstring? x = null;double price = 0.1 + 0.2;ใช้กับเงิน → ใช้decimalstring == "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 — ต้องระวังตรงนี้เป็นพิเศษ
- ✅ เช็ค null + ว่าง: ใช้
(int)null— error (value type ไม่รับ null)int.Parse("abc")→ FormatException → ใช้TryParse- เก็บเงินด้วย
double→ rounding ใน production = bug ใหญ่ - ใช้
ArrayList— ของเก่า legacy, boxing → ใช้List<T>
🛠️ Checkpoint 2 — ลงมือทำ
- สร้างโปรเจกต์ที่รับ 2 จำนวนจาก stdin → คำนวณ + − × ÷ → print
- ใช้
decimalสำหรับ "เงิน" + format{x:C}(currency) - ถ้า input ไม่ใช่ตัวเลข → ใช้
TryParse+ แจ้ง error สวย ๆ - สร้าง struct
Moneyที่มี fielddecimal Amount+string Currencyและ classAccountที่มี fieldstring Owner+Money Balance(รูปแบบ field ดูตัวอย่าง §3 — primary constructor ของ struct/class จะเรียนในบท 5/10) - 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?vsstring - ใช้
varเมื่อ type ชัดเจน Parse→TryParseเสมอเมื่อ input อาจผิด