☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
C# 基礎 中級

📁 檔案 I/O 與序列化

📌 為什麼要學檔案操作?

程式中的資料都存在記憶體裡,一關機就消失了。檔案 I/O 讓你把資料永久保存到硬碟上。

想像你的程式是一個廚師

  • 記憶體是工作檯面上的食材(隨時可用,但打烊就收走了)
  • 檔案系統是冰箱和儲藏室(可以長期保存,但取用要多花一點時間)
  • 序列化就是把食材打包裝袋的過程(把物件轉成可儲存的格式)

📖 基本檔案讀寫

File.ReadAllText / WriteAllText

// 寫入文字到檔案(像把信放進信封裡)
string content = "Hello, C#!這是我的第一個檔案。"; // 要寫入的內容
File.WriteAllText("hello.txt", content); // 寫入檔案,如果檔案不存在會自動建立

// 讀取整個檔案內容(像把信從信封裡拿出來看)
string readContent = File.ReadAllText("hello.txt"); // 讀取整個檔案
Console.WriteLine(readContent); // 印出檔案內容

// 寫入多行(像在紙上一行一行寫)
string[] lines = { "第一行", "第二行", "第三行" }; // 要寫入的多行文字
File.WriteAllLines("lines.txt", lines); // 每個元素寫成一行

// 讀取多行(像一行一行讀信)
string[] readLines = File.ReadAllLines("lines.txt"); // 讀取所有行
foreach (string line in readLines) // 走訪每一行
{
    Console.WriteLine(line); // 印出每一行
}

// 附加內容到檔案末尾(像在信的最後面繼續寫)
File.AppendAllText("hello.txt", "\n新增的內容"); // 不會覆蓋原本的內容

📊 StreamReader 與 StreamWriter

當檔案很大時,一次全部讀進記憶體不是好主意。Stream 就像一條水管——資料像水一樣一點一點流過來。

// 使用 StreamWriter 寫入(像打開水龍頭,一滴一滴寫入)
using (StreamWriter writer = new StreamWriter("log.txt")) // using 確保寫完後自動關閉檔案
{
    writer.WriteLine("2024-01-15 09:00 系統啟動"); // 寫入第一行
    writer.WriteLine("2024-01-15 09:01 使用者登入"); // 寫入第二行
    writer.WriteLine("2024-01-15 09:05 查詢商品"); // 寫入第三行
} // using 結束時自動呼叫 Dispose(),關閉檔案

// 使用 StreamReader 逐行讀取(像水管接水,一滴一滴接)
using (StreamReader reader = new StreamReader("log.txt")) // using 確保讀完後自動關閉
{
    string? line; // 儲存每一行的變數(可能是 null)
    while ((line = reader.ReadLine()) != null) // 一行一行讀,直到沒有資料
    {
        Console.WriteLine(line); // 印出每一行
    }
} // using 結束時自動關閉檔案

// 更簡潔的 using 宣告(C# 8+)
using var writer2 = new StreamWriter("output.txt"); // 不需要大括號
writer2.WriteLine("簡潔的寫法"); // 寫入一行
writer2.WriteLine("到變數離開作用域時自動關閉"); // 寫入另一行
// writer2 在方法結束時自動 Dispose

📂 Path 類別:路徑的好幫手

// Path 類別專門處理檔案路徑(像 GPS 導航,幫你組合正確的路徑)
string folder = @"C:\Users\Documents"; // 資料夾路徑
string fileName = "report.xlsx";       // 檔案名稱

// Combine:安全地組合路徑(不用自己加 \ 符號)
string fullPath = Path.Combine(folder, fileName); // "C:\Users\Documents\report.xlsx"

// GetFileName:取得檔案名稱(從完整路徑中擷取最後一段)
string name = Path.GetFileName(fullPath); // "report.xlsx"

// GetFileNameWithoutExtension:取得不含副檔名的檔案名稱
string nameOnly = Path.GetFileNameWithoutExtension(fullPath); // "report"

// GetExtension:取得副檔名
string ext = Path.GetExtension(fullPath); // ".xlsx"

// GetDirectoryName:取得所在資料夾路徑
string dir = Path.GetDirectoryName(fullPath); // "C:\Users\Documents"

// ChangeExtension:變更副檔名(像把 .doc 改成 .pdf)
string pdfPath = Path.ChangeExtension(fullPath, ".pdf"); // "C:\Users\Documents\report.pdf"

📁 Directory 類別:資料夾操作

// 建立資料夾(像蓋一個新房間,如果已經存在就不會出錯)
Directory.CreateDirectory(@"C:\MyApp\Data\Logs"); // 會自動建立中間不存在的資料夾

// 檢查資料夾是否存在(像看看房間在不在)
bool exists = Directory.Exists(@"C:\MyApp\Data"); // true 或 false

// 取得資料夾中所有檔案(像打開房間看裡面有什麼東西)
string[] files = Directory.GetFiles(@"C:\MyApp\Data"); // 取得所有檔案路徑
foreach (string file in files) // 走訪每個檔案
{
    Console.WriteLine(Path.GetFileName(file)); // 只印出檔案名稱
}

// 用篩選條件找特定類型的檔案(像只找房間裡的書)
string[] txtFiles = Directory.GetFiles(@"C:\MyApp\Data", "*.txt"); // 只找 .txt 檔案

// 遞迴搜尋所有子資料夾(像搜遍整棟大樓的每個房間)
string[] allFiles = Directory.GetFiles(
    @"C:\MyApp",        // 從哪裡開始找
    "*.log",            // 要找什麼類型
    SearchOption.AllDirectories // 包含所有子資料夾
);

// 取得所有子資料夾(像看大樓裡有哪些房間)
string[] subDirs = Directory.GetDirectories(@"C:\MyApp"); // 取得子資料夾列表

🔄 JSON 序列化:System.Text.Json

序列化就是把程式中的物件變成文字格式(JSON),這樣才能存到檔案或透過網路傳送。就像把立體的蛋糕拍成照片,方便分享給別人,別人再根據照片(反序列化)重新做出蛋糕。

using System.Text.Json; // 引用 JSON 序列化命名空間
using System.Text.Json.Serialization; // 引用 JSON 屬性標註命名空間

// 定義一個資料模型
public class Product // 商品類別
{
    [JsonPropertyName("name")]    // 指定 JSON 中的屬性名稱為 "name"
    public string Name { get; set; } = ""; // 商品名稱

    [JsonPropertyName("price")]   // 指定 JSON 中的屬性名稱為 "price"
    public decimal Price { get; set; } // 商品價格

    [JsonPropertyName("inStock")] // 指定 JSON 中的屬性名稱為 "inStock"
    public bool InStock { get; set; } // 是否有庫存

    [JsonIgnore] // 序列化時忽略此屬性(不會出現在 JSON 中)
    public string InternalCode { get; set; } = ""; // 內部代碼
}

// 序列化:物件 → JSON 字串(拍照)
Product product = new Product // 建立商品物件
{
    Name = "MacBook Pro", // 設定名稱
    Price = 59900,          // 設定價格
    InStock = true          // 設定庫存狀態
};

// 設定序列化選項
var options = new JsonSerializerOptions
{
    WriteIndented = true,                               // 格式化輸出(縮排,方便閱讀)
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,  // 屬性名稱用 camelCase
    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 允許中文不被編碼
};

// 將物件序列化為 JSON 字串
string json = JsonSerializer.Serialize(product, options); // 把物件變成 JSON
Console.WriteLine(json); // 印出 JSON 字串
// 輸出:
// {
//   "name": "MacBook Pro",
//   "price": 59900,
//   "inStock": true
// }

// 存到檔案
File.WriteAllText("product.json", json); // 將 JSON 字串寫入檔案

反序列化:JSON → 物件

// 從檔案讀取 JSON 並還原成物件(看照片重做蛋糕)
string jsonFromFile = File.ReadAllText("product.json"); // 從檔案讀取 JSON 字串

// 反序列化:JSON 字串 → 物件
Product? loaded = JsonSerializer.Deserialize<Product>(jsonFromFile, options);
// 用泛型指定要還原成什麼類型

if (loaded != null) // 確認反序列化成功(不是 null)
{
    Console.WriteLine($"商品:{loaded.Name},價格:{loaded.Price}");
    // 印出:"商品:MacBook Pro,價格:59900"
}

// 處理集合(多個物件)
List<Product> products = new List<Product> // 建立商品清單
{
    new Product { Name = "iPhone", Price = 35900, InStock = true },   // 第一個商品
    new Product { Name = "iPad", Price = 27900, InStock = false },    // 第二個商品
    new Product { Name = "AirPods", Price = 7490, InStock = true }    // 第三個商品
};

// 序列化整個清單
string jsonArray = JsonSerializer.Serialize(products, options); // 清單 → JSON 陣列
File.WriteAllText("products.json", jsonArray); // 存到檔案

// 反序列化 JSON 陣列
string arrayJson = File.ReadAllText("products.json"); // 從檔案讀取
List<Product>? loadedList = JsonSerializer.Deserialize<List<Product>>(arrayJson, options);
// JSON 陣列 → List<Product>

if (loadedList != null) // 確認不是 null
{
    foreach (var p in loadedList) // 走訪每個商品
    {
        Console.WriteLine($"{p.Name}: ${p.Price}"); // 印出商品資訊
    }
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:沒有使用 using 語句

// ❌ 錯誤寫法:沒有用 using,檔案可能不會正確關閉
StreamWriter writer = new StreamWriter("data.txt"); // 開啟檔案
writer.WriteLine("重要資料"); // 寫入資料
// 忘記呼叫 writer.Close() 或 writer.Dispose()!
// 如果中間發生例外,檔案會被鎖住無法被其他程式存取
// ✅ 正確寫法:使用 using 語句確保資源被釋放
using var writer = new StreamWriter("data.txt"); // using 確保自動關閉
writer.WriteLine("重要資料"); // 寫入資料
// 即使發生例外,using 也會確保檔案被正確關閉

解釋: 開啟檔案就像借了一把鑰匙——用完一定要歸還。using 語句就像自動歸還機制,不管發生什麼事都會把鑰匙還回去。如果忘記歸還(沒有 Dispose),其他人(程式)就無法使用這個檔案。

❌ 錯誤 2:硬編碼檔案路徑

// ❌ 錯誤寫法:直接寫死路徑(換台電腦就爆了)
string path = @"C:\Users\小明\Desktop\data.txt"; // 只在小明的電腦上有效
File.ReadAllText(path); // 換台電腦就會 FileNotFoundException
// ✅ 正確寫法:使用相對路徑或動態取得路徑
// 方法 1:使用應用程式所在目錄
string appDir = AppDomain.CurrentDomain.BaseDirectory; // 取得程式所在路徑
string path1 = Path.Combine(appDir, "data.txt"); // 組合出完整路徑

// 方法 2:使用特殊資料夾路徑
string desktopPath = Environment.GetFolderPath(
    Environment.SpecialFolder.Desktop // 取得桌面路徑(每台電腦都不同)
);
string path2 = Path.Combine(desktopPath, "data.txt"); // 組合路徑

// 方法 3:使用當前目錄
string currentDir = Directory.GetCurrentDirectory(); // 取得目前工作目錄
string path3 = Path.Combine(currentDir, "data", "config.json"); // 組合多層路徑

解釋: 硬編碼路徑就像把你家地址刻在 GPS 上,然後把 GPS 借給住在不同城市的朋友用——當然會找不到路。使用 Path.Combine 和環境變數可以讓你的程式在任何電腦上都能正確找到檔案。

❌ 錯誤 3:沒有處理 FileNotFoundException

// ❌ 錯誤寫法:直接讀取,不管檔案存不存在
string content = File.ReadAllText("config.json"); // 如果檔案不存在就會爆炸!
// ✅ 正確寫法:先檢查再讀取,或用 try-catch
// 方法 1:先檢查檔案是否存在
if (File.Exists("config.json")) // 先確認檔案存在
{
    string content = File.ReadAllText("config.json"); // 才去讀取
    Console.WriteLine(content); // 印出內容
}
else
{
    Console.WriteLine("設定檔不存在,使用預設值"); // 給個友善的訊息
}

// 方法 2:用 try-catch 捕捉例外
try
{
    string content = File.ReadAllText("config.json"); // 嘗試讀取
    Console.WriteLine(content); // 印出內容
}
catch (FileNotFoundException ex) // 捕捉「檔案不存在」的例外
{
    Console.WriteLine($"找不到檔案:{ex.FileName}"); // 印出錯誤訊息
}
catch (UnauthorizedAccessException) // 捕捉「沒有權限」的例外
{
    Console.WriteLine("沒有權限讀取此檔案"); // 印出提示
}

解釋: 不先確認檔案存在就直接讀取,就像不看路就過馬路一樣危險。永遠要假設檔案可能不存在、可能被佔用、可能沒有讀取權限。try-catch 就像安全氣囊,出意外時能保護你的程式不會整個崩潰。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊