const 與 readonly 都是 C# 定義「不可變更值」的關鍵字,但其語意、初始化時機、記憶體配置、使用場景有本質差異。本文透過程式碼實測、IL 反組譯、版本相容性分析,徹底釐清兩者差異。
| 特性 | const (常數) | readonly (唯讀欄位) |
|---|---|---|
| 初始化時機 | 編譯期 必須在宣告時指定常值 | 執行期 宣告時或建構函式中指定 |
| 可接受運算式 | 僅限常值運算式 (字面值、其他 const、基本運算) | 任意運算式、方法呼叫、new 物件、設定檔讀取 |
| 靜態性 | 隱含 static (屬於類型,非實例) | 預設為實例欄位,可加 static 變類型級 |
| 存取方式 | TypeName.ConstName (不需實例) | 實例欄位需 instance.Field,static 則 TypeName.Field |
| 不佔用實例記憶體,編譯期內嵌至呼叫端 IL | 佔用實例/類型記憶體,執行期讀取 | |
| 修飾對象 | 欄位、區域變數 | 僅限欄位 (不可用於區域變數/屬性) |
| 版本相容性 | ⚠️ 修改值需重新編譯所有呼叫端 | ✅ 修改值只需重新編譯定義端 |
| 適用型別 | 內建型別 + string + enum + null | 任意型別 (含陣列、List、自訂類別、結構) |
public class ConstantsDemo
{
// const: 編譯期常數
public const double PI = 3.141592653589793;
public const string AppName = "MyApp";
public const int MaxRetries = 3;
// readonly: 執行期唯讀
public readonly DateTime CreatedAt = DateTime.Now; // 宣告時初始化
public readonly Guid InstanceId; // 建構函式初始化
public readonly string ConfigPath; // 複雜邏輯初始化
public static readonly string[] SupportedFormats = { "json", "xml", "yaml" }; // 靜態唯讀陣列
public ConstantsDemo(string configBasePath)
{
InstanceId = Guid.NewGuid(); // 唯一建構函式可指派
ConfigPath = Path.Combine(configBasePath, "settings.json");
// CreatedAt = DateTime.UtcNow; // ❌ 編譯錯誤:readonly 欄位只能在宣告或建構函式指派
}
// readonly 不能用於屬性
// public readonly string Name { get; set; } // ❌ 編譯錯誤
// const 不能用於參考型別 (string 除外)
// public const List DefaultList = new List(); // ❌ 編譯錯誤
}
// const 欄位:編譯期直接內嵌數值,IL 中無欄位定義 .field public static literal float64 PI = 3.1415926535897931 .field public static literal string AppName = "MyApp" // readonly 欄位:保留欄位定義,initonly 標記 .field public initonly valuetype [System.Runtime]System.DateTime CreatedAt .field public initonly valuetype [System.Runtime]System.Guid InstanceId .field public static initonly string[] SupportedFormats
關鍵差異: literal (const) vs initonly (readonly)。呼叫端 IL 會直接內嵌 const 數值,而 readonly 需執行期讀取欄位。
// Library.dll v1.0
public class Config { public const int MaxConnections = 100; }
// App.exe 參考 Library.dll v1.0
// 編譯後 App.exe IL 內嵌:ldc.i4.s 100 (直接寫死 100)
// Library.dll v1.1 修改常數
public class Config { public const int MaxConnections = 200; }
// ❌ 只更新 Library.dll 不重新編譯 App.exe
// App.exe 仍使用 100,因為 IL 已內嵌常值!
// 改用 readonly
public class Config { public static readonly int MaxConnections = 100; }
// App.exe IL:ldsfld int32 Config::MaxConnections (執行期讀取欄位)
// ✅ 只更新 Library.dll,App.exe 自動讀取新值 200
鐵律: 公開給外部組件使用的常值,必須用 static readonly,絕不可用 const
graph TD
A[需要不可變更的值] --> B{值在編譯期就能確定嗎?}
B -->|是| C{型別是內建型別/string/enum?}
C -->|是| D{會被外部組件/元件引用嗎?}
D -->|否| E[使用 const ✅]
D -->|是| F[使用 static readonly ✅]
C -->|否 (陣列/物件/集合)| F
B -->|否 (需執行期計算/讀取設定/新物件)| F
F --> G[readonly (實例) 或 static readonly (類型級)]
| 場景 | 錯誤用法 | 正確用法 |
|---|---|---|
| 資料庫連接字串 | const string ConnStr = "..."; | static readonly string ConnStr = ConfigurationManager.ConnectionStrings["DB"].ConnectionString; |
| 預設分頁大小 | const int PageSize = 20; (內部專用可接受) | static readonly int PageSize = 20; (較安全) |
| HTTP 狀態碼 | const int OK = 200; | const int OK = 200; (標準不變、內建型別、無版本問題) |
| 正則表達式 | const string EmailRegex = @"..."; | static readonly Regex EmailRegex = new Regex(@"...", RegexOptions.Compiled); |
| 預設選項集合 | const string[] Options = { "A", "B" }; | static readonly string[] Options = { "A", "B" }; 或 static readonly IReadOnlyList<string> Options = ... |
| 建構函式注入的服務 | 不可行 | readonly IEmailService _emailService; (在建構函式指派) |
// C# 7.2+ readonly struct:結構體本身不可變,效能優於 class
public readonly struct Point3D
{
public double X { get; }
public double Y { get; }
public double Z { get; }
public Point3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);
// 編譯器保證不會修改 this,可安全傳遞給 in 參數
public double DistanceTo(in Point3D other)
=> Math.Sqrt((X-other.X)*(X-other.X) + (Y-other.Y)*(Y-other.Y) + (Z-other.Z)*(Z-other.Z));
}
// C# 9+ init-only 屬性:物件初始化器後不可變
public class Settings
{
public string ApiKey { get; init; }
public int Timeout { get; init; } = 30;
}
// 使用
var s = new Settings { ApiKey = "abc", Timeout = 60 };
// s.ApiKey = "xyz"; // ❌ 編譯錯誤
readonly struct、init 屬性、record 類型建構不可變資料模型正確選擇 const 與 readonly,不只是語法正確,更關係到元件版本演進的相容性與系統長期維護性。