1. 介绍
在 Windows(不只是 Windows)中,每个文件和目录都有一组属性(元数据):完整路径、名字、扩展名、大小、创建/修改/访问时间、属性等。想象一下:文件不仅仅是字节流,它还有一张可以用 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 = { "Б", "КБ", "МБ", "ГБ", "ТБ" };
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. 元数据的实用场景
按日期查找文件
查找在指定时间段内修改过的文件(适合清理、分析或审计)。
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);
查找重复文件
快速方法是按大小分组。要更精确可以加上内容哈希比较。
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(); // 刷新元数据
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