1. 介紹
在 Windows(以及其他系統)中,每個檔案和目錄都有一組屬性(metadata):完整路徑、名稱、副檔名、大小、建立/修改/存取時間、屬性等等。想像一下:檔案不只是位元組,還是一張可以用 FileInfo 和 DirectoryInfo 讀取的完整資料表。
| 屬性 | 說明 | 範例 |
|---|---|---|
| 完整路徑 | 檔案/資料夾的完整名稱 | |
| 名稱 | 不含路徑的名稱 | |
| 副檔名 | .txt, .csv, .jpg 等等 | |
| 大小 (Bytes) | 檔案大小(位元組) | |
| 建立時間 | 檔案/資料夾被建立的時間 | |
| 修改時間 | 最後一次變更內容的時間 | |
| 屬性 | 例如唯讀、隱藏等 | |
| 父資料夾 | 包含該檔案/目錄的資料夾 | |
2. 深入檔案屬性操作
時間戳記的詳細分析
屬性 CreationTime、LastWriteTime、LastAccessTime 會回傳 DateTime,它們的行為會依檔案系統與對檔案的操作而異。
var fileInfo = new FileInfo("document.txt");
if (fileInfo.Exists)
{
Console.WriteLine($"已建立: {fileInfo.CreationTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"已修改: {fileInfo.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"最後存取: {fileInfo.LastAccessTime:yyyy-MM-dd HH:mm:ss}");
// 建立與最後修改的差異
var age = fileInfo.LastWriteTime - fileInfo.CreationTime;
Console.WriteLine($"檔案變更持續了: {age.TotalDays:F1} 天");
}
有趣的是,複製檔案通常會把建立時間更新為當前時間,但修改時間可能保留原始值。這一點對備份和活動分析很重要。
處理副檔名與檔名
屬性 FullName、Name 與 Extension 看起來很簡單,但有些細節要注意:沒有副檔名、複合副檔名像 .tar.gz、以及以點開頭的隱藏檔。
var files = new[]
{
new FileInfo("document.txt"),
new FileInfo("archive.tar.gz"),
new FileInfo("README"),
new FileInfo(".gitignore")
};
foreach (var file in files)
{
Console.WriteLine($"完整名稱: {file.FullName}");
Console.WriteLine($"名稱: {file.Name}");
Console.WriteLine($"副檔名: '{file.Extension}'");
// 不含副檔名的名稱
string nameWithoutExtension = Path.GetFileNameWithoutExtension(file.Name);
Console.WriteLine($"不含副檔名的名稱: {nameWithoutExtension}");
Console.WriteLine("---");
}
檔案大小與格式化
屬性 Length 回傳位元組;對使用者來說,用 KB/MB/GB 顯示會更友善。輔助函式範例如下:
static string FormatFileSize(long bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
int counter = 0;
decimal number = bytes;
while (Math.Round(number / 1024) >= 1)
{
number /= 1024;
counter++;
}
return $"{number:N1} {suffixes[counter]}";
}
// 使用範例
var file = new FileInfo("bigfile.zip");
if (file.Exists)
{
Console.WriteLine($"檔案大小: {FormatFileSize(file.Length)}");
}
3. 進階檔案屬性操作
屬性由列舉型別 FileAttributes 表示(位元旗標),因此一個檔案可以同時有多個屬性。用 HasFlag 來檢查會很方便。
var fileInfo = new FileInfo("important.txt");
Console.WriteLine($"檔案屬性: {fileInfo.Attributes}");
// 檢查特定屬性
if (fileInfo.Attributes.HasFlag(FileAttributes.Hidden))
{
Console.WriteLine("檔案被隱藏了!");
}
if (fileInfo.Attributes.HasFlag(FileAttributes.ReadOnly))
{
Console.WriteLine("檔案為唯讀!");
}
if (fileInfo.Attributes.HasFlag(FileAttributes.System))
{
Console.WriteLine("這是系統檔案!");
}
屬性也可以程式化地修改:
// 設為隱藏檔
fileInfo.Attributes |= FileAttributes.Hidden;
// 移除「唯讀」屬性
fileInfo.Attributes &= ~FileAttributes.ReadOnly;
// 一次設定多個屬性
fileInfo.Attributes = FileAttributes.ReadOnly | FileAttributes.Hidden;
4. 進階目錄操作
用樣板搜尋檔案
方法 GetFiles() 和 GetDirectories() 接受樣板字串,能幫助過濾內容。
var dir = new DirectoryInfo(@"C:\Projects");
if (dir.Exists)
{
// 找到所有文字檔
var textFiles = dir.GetFiles("*.txt");
Console.WriteLine($"找到的文字檔數量: {textFiles.Length}");
// 找到所有以 "temp" 開頭的檔案
var tempFiles = dir.GetFiles("temp*");
// 找到所有圖片檔案
var imageExtensions = new[] { "*.jpg", "*.png", "*.gif", "*.bmp" };
var allImages = imageExtensions.SelectMany(ext => dir.GetFiles(ext)).ToArray();
Console.WriteLine($"找到的圖片數量: {allImages.Length}");
}
在子目錄中遞迴搜尋
要遍歷所有子目錄可以使用 SearchOption.AllDirectories。
var dir = new DirectoryInfo(@"C:\Development");
// 找出所有子資料夾中的 C# 檔
var csharpFiles = dir.GetFiles("*.cs", SearchOption.AllDirectories);
Console.WriteLine($"總共找到 .cs 檔: {csharpFiles.Length}");
// 顯示前 10 個檔案與其路徑
foreach (var file in csharpFiles.Take(10))
{
Console.WriteLine($"{file.FullName} ({FormatFileSize(file.Length)})");
}
目錄內容分析
一個彙總分析範例:檔案/資料夾數量、總大小、按副檔名分布,以及前五大檔案。
static void AnalyzeDirectory(DirectoryInfo dir)
{
if (!dir.Exists)
{
Console.WriteLine("目錄不存在!");
return;
}
var files = dir.GetFiles();
var subdirs = dir.GetDirectories();
Console.WriteLine($"分析目錄: {dir.FullName}");
Console.WriteLine($"檔案數: {files.Length}, 子目錄數: {subdirs.Length}");
if (files.Length == 0)
{
Console.WriteLine("找不到檔案。");
return;
}
long totalSize = files.Sum(f => f.Length);
Console.WriteLine($"檔案總大小: {FormatFileSize(totalSize)}");
// 按副檔名分組
var byExtension = files.GroupBy(f => f.Extension.ToLower())
.OrderByDescending(g => g.Sum(f => f.Length));
Console.WriteLine("\n按檔案類型分布:");
foreach (var group in byExtension)
{
string ext = string.IsNullOrEmpty(group.Key) ? "(無副檔名)" : group.Key;
long groupSize = group.Sum(f => f.Length);
Console.WriteLine($" {ext}: {group.Count()} 個檔案, {FormatFileSize(groupSize)}");
}
// 前五大檔案
var largestFiles = files.OrderByDescending(f => f.Length).Take(5);
Console.WriteLine("\n最大的檔案:");
foreach (var file in largestFiles)
{
Console.WriteLine($" {file.Name}: {FormatFileSize(file.Length)}");
}
}
5. 優化的目錄大小計算
對於大型資料夾,避免使用 GetFiles() 一次載入所有項目,改用延遲列舉的 EnumerateFiles()/EnumerateDirectories(),並處理例外,同時可顯示進度。
static long GetDirectorySizeAdvanced(DirectoryInfo dir, bool showProgress = false)
{
long totalSize = 0;
int fileCount = 0;
var inaccessibleDirs = new List<string>();
try
{
// 對於大型目錄使用 EnumerateFiles(延遲載入)
foreach (var file in dir.EnumerateFiles())
{
try
{
totalSize += file.Length;
fileCount++;
if (showProgress && fileCount % 1000 == 0)
{
Console.WriteLine($"已處理檔案數: {fileCount}, 大小: {FormatFileSize(totalSize)}");
}
}
catch (UnauthorizedAccessException)
{
// 檔案無法存取,跳過
}
catch (IOException)
{
// 讀取檔案時出問題,跳過
}
}
// 遞迴處理子目錄
foreach (var subdir in dir.EnumerateDirectories())
{
try
{
totalSize += GetDirectorySizeAdvanced(subdir, showProgress);
}
catch (UnauthorizedAccessException)
{
inaccessibleDirs.Add(subdir.FullName);
}
}
}
catch (UnauthorizedAccessException)
{
inaccessibleDirs.Add(dir.FullName);
}
if (inaccessibleDirs.Any() && showProgress)
{
Console.WriteLine($"無法存取的目錄數: {inaccessibleDirs.Count}");
}
return totalSize;
}
// 使用範例
var targetDir = new DirectoryInfo(@"C:\Users");
Console.WriteLine("開始計算目錄大小...");
long size = GetDirectorySizeAdvanced(targetDir, showProgress: true);
Console.WriteLine($"總大小: {FormatFileSize(size)}");
6. metadata 的實用應用
按日期搜尋檔案
搜尋在指定期間內被修改的檔案(對清理、分析或稽核很方便)。
static void FindFilesByDate(DirectoryInfo dir, DateTime fromDate, DateTime toDate)
{
Console.WriteLine($"搜尋檔案:從 {fromDate:yyyy-MM-dd} 到 {toDate:yyyy-MM-dd}");
var matchingFiles = dir.GetFiles("*", SearchOption.AllDirectories)
.Where(f => f.LastWriteTime >= fromDate && f.LastWriteTime <= toDate)
.OrderByDescending(f => f.LastWriteTime);
Console.WriteLine($"找到檔案數: {matchingFiles.Count()}");
foreach (var file in matchingFiles.Take(20))
{
Console.WriteLine($"{file.LastWriteTime:yyyy-MM-dd HH:mm} - {file.Name} ({FormatFileSize(file.Length)})");
}
}
// 範例:找到過去一週內修改的所有檔案
var dir = new DirectoryInfo(@"C:\Documents");
var weekAgo = DateTime.Now.AddDays(-7);
FindFilesByDate(dir, weekAgo, DateTime.Now);
尋找重複檔案
快速方法是先按大小分組;要更精準可以再比對內容的雜湊值(hash)。
static void FindPotentialDuplicates(DirectoryInfo dir)
{
Console.WriteLine($"在 {dir.FullName} 搜尋潛在重複檔案");
var files = dir.GetFiles("*", SearchOption.AllDirectories)
.Where(f => f.Length > 0) // 排除空檔案
.GroupBy(f => f.Length)
.Where(g => g.Count() > 1) // 只保留有多個檔案的群組
.OrderByDescending(g => g.Key); // 以大小排序
foreach (var sizeGroup in files.Take(10))
{
Console.WriteLine($"\n大小為 {FormatFileSize(sizeGroup.Key)} 的檔案 ({sizeGroup.Count()} 個):");
foreach (var file in sizeGroup)
{
Console.WriteLine($" {file.FullName}");
Console.WriteLine($" 已修改: {file.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
}
}
}
監控目錄變更
顯示在最近 N 分鐘內被修改的檔案。
static void MonitorRecentChanges(DirectoryInfo dir, int minutesBack = 60)
{
var cutoffTime = DateTime.Now.AddMinutes(-minutesBack);
var recentFiles = dir.GetFiles("*", SearchOption.AllDirectories)
.Where(f => f.LastWriteTime > cutoffTime)
.OrderByDescending(f => f.LastWriteTime);
Console.WriteLine($"在過去 {minutesBack} 分鐘內被修改的檔案:");
if (!recentFiles.Any())
{
Console.WriteLine("沒有發現變更。");
return;
}
foreach (var file in recentFiles)
{
var minutesAgo = (DateTime.Now - file.LastWriteTime).TotalMinutes;
Console.WriteLine($"{file.Name} - {minutesAgo:F0} 分鐘前 ({FormatFileSize(file.Length)})");
}
}
7. 操作父目錄
FileInfo 的屬性 Directory 與 DirectoryInfo 的 Parent 可以用來往上走層級。
var file = new FileInfo(@"C:\Projects\MyApp\src\Program.cs");
Console.WriteLine($"檔案: {file.Name}");
Console.WriteLine($"資料夾: {file.Directory.Name}");
Console.WriteLine($"父資料夾: {file.Directory.Parent.Name}");
Console.WriteLine($"專案根資料夾: {file.Directory.Parent.Parent.Name}");
// 可以一直往上走到根目錄
var currentDir = file.Directory;
while (currentDir.Parent != null)
{
Console.WriteLine($"層級: {currentDir.Name}");
currentDir = currentDir.Parent;
}
Console.WriteLine($"根目錄: {currentDir.Name}");
8. 陷阱與常見錯誤
1. 快取。物件 FileInfo 與 DirectoryInfo 會快取值。如果在建立物件後檔案被修改,資料可能已過時。使用 Refresh() 來更新。
var file = new FileInfo("test.txt");
file.Refresh(); // 更新 metadata
2. 存取例外。有些檔案和資料夾無法存取:務必處理 UnauthorizedAccessException 與其他存取錯誤。
try
{
var files = new DirectoryInfo(path).GetFiles();
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("無存取權限");
}
3. 時間戳記。複製時 CreationTime 可能會變更,而 LastWriteTime 可能保留。這會影響報表與同步演算法。
File.Copy("a.txt", "b.txt");
4. 效能。 GetFiles() 會一次載入所有結果,在大型目錄上可能會很慢。對於延遲列舉,偏好使用 EnumerateFiles()。
GO TO FULL VERSION