C# P/Invoke文件API C#如何调用Windows API执行高级文件操作

来源:这里教程网 时间:2026-02-21 17:42:10 作者:

为什么直接用
CreateFile
FileStream
更适合绕过 .NET 文件锁或控制缓存行为

.NET 的

FileStream
默认启用内核缓冲(
FILE_FLAG_SEQUENTIAL_SCAN
隐式生效),且无法直接指定
FILE_FLAG_NO_BUFFERING
FILE_FLAG_WRITE_THROUGH
。当你需要强制直写磁盘、跳过系统缓存,或以独占/备份方式打开被其他进程锁定的文件(比如正在被记事本编辑的 .log),就必须用 P/Invoke 调用原始
CreateFile

关键点:

CreateFile
返回的是 Windows
HANDLE
,不是 .NET
SafeHandle
,必须手动配对
CloseHandle
,否则资源泄漏
文件路径必须是 Unicode(
lpFileName
string
即可,P/Invoke 自动转 UTF-16)
若要打开正在被其他程序以
FILE_SHARE_DELETE
以外方式共享的文件,需显式传
FILE_SHARE_READ | FILE_SHARE_WRITE
权限标志(
dwDesiredAccess
)不能只传
GENERIC_READ
就去写——写操作必须含
GENERIC_WRITE
,哪怕你后续只调
WriteFile

CopyFileEx
如何实现带进度回调和可取消的大文件拷贝

.NET 的

File.Copy
是阻塞式、无进度、不可中断的。而 Windows 原生
CopyFileEx
支持回调函数(
LPPROGRESS_ROUTINE
)和取消句柄(
hCancel
),适合 UI 场景。

实操要点:

回调函数必须标记为
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
,否则在 x64 下崩溃
回调中不能调用任何托管堆分配操作(如
Console.WriteLine
),推荐只更新线程安全变量(如
Interlocked
)或发
BeginInvoke
到 UI 线程
hCancel
是一个手动重置事件(
CreateEvent
创建),调用方在需要中止时
SetEvent(hCancel)
必须传
COPY_FILE_FAIL_IF_EXISTS
等标志位来控制覆盖逻辑,.NET 的
overwrite: true
不会自动映射

GetFileInformationByHandle
读取 NTFS 文件 ID 和硬链接数

想获取文件唯一标识(比如判断两个路径是否指向同一文件实体)、或检查是否为硬链接/符号链接,

FileInfo
完全没提供这些字段。Windows 内核对象的
BY_HANDLE_FILE_INFORMATION
结构体包含
nFileIndexLow/High
(即 File ID)和
nNumberOfLinks
(硬链接计数)。

注意细节:

必须先用
CreateFile
FILE_READ_ATTRIBUTES
权限打开句柄,不能用
File.OpenHandle
(它返回的是封装过的
SafeFileHandle
,不暴露原生 HANDLE)
nFileIndexLow + nFileIndexHigh
组合才是完整 128-bit File ID,仅比对 low part 可能冲突
nNumberOfLinks == 1
不代表没有硬链接——某些场景(如卷影复制)下该值可能不准,建议结合
GetFinalPathNameByHandle
验证
结构体中的
dwVolumeSerialNumber
必须与当前卷序列号一致,才能确认 File ID 有效(跨卷移动后 ID 会变)

常见崩溃点:字符串编码、结构体对齐、错误码误判

P/Invoke 调用失败不抛托管异常,而是靠

Marshal.GetLastWin32Error()
拿错误码。但这个值极易被中间的托管调用污染(比如日志写入、GC 触发)。

务必做到:

所有 P/Invoke 声明加
SetLastError = true
,并在调用后**立刻**调用
Marshal.GetLastWin32Error()
StringBuilder
传给 API(如
GetFinalPathNameByHandle
)前,必须先
.Capacity
预设足够空间(NTFS 路径最长 32767 字符),否则缓冲区溢出
自定义结构体(如
BY_HANDLE_FILE_INFORMATION
)必须加
[StructLayout(LayoutKind.Sequential, Pack = 4)]
,否则 x64 下字段偏移错乱
不要用
string
接收宽字符输出参数(如
GetVolumePathName
),必须用
StringBuilder
并指定
CharSet = CharSet.Unicode

最常被忽略的是:

INVALID_HANDLE_VALUE
是 -1,但 C# 中
IntPtr
-1
(IntPtr)(-1)
在某些运行时版本下比较行为不一致,应统一用
handle.ToInt64() == -1
判定。

相关推荐