Поделиться через


Форматы путей к файлам в системах Windows

Члены многих типов в System.IO пространстве имен включают path параметр, позволяющий указать абсолютный или относительный путь к ресурсу файловой системы. Затем этот путь передается в API файловой системы Windows. В этом разделе рассматриваются форматы путей к файлам, которые можно использовать в системах Windows.

Традиционные пути DOS

Стандартный путь DOS может состоять из трех компонентов:

Если присутствуют все три компонента, путь является абсолютным. Если не указана буква тома или диска, а имя каталога начинается с символа разделителя каталогов, путь отсчитывается относительно корня текущего диска. В противном случае путь относительно текущего каталога. В следующей таблице показаны возможные пути к каталогу и файлу.

Путь Описание
C:\Documents\Newsletters\Summer2018.pdf Абсолютный путь к файлу из корневого диска C:.
\Program Files\Custom Utilities\StringFinder.exe Относительный путь от корня текущего диска.
2018\January.xlsx Относительный путь к файлу в подкаталоге текущего каталога.
..\Publications\TravelBrochure.pdf Относительный путь к файлу в каталоге, начиная с текущего каталога.
C:\Projects\apilibrary\apilibrary.sln Абсолютный путь к файлу из корневого диска C:.
C:Projects\apilibrary\apilibrary.sln Относительный путь от текущего каталога диска C:.

Это важно

Обратите внимание на разницу между последними двумя путями. Оба указывают необязательный описатель тома (C: в обоих случаях), но первый начинается с корня указанного тома, а второй — нет. В результате первый — абсолютный путь из корневого каталога диска C:, а второй — относительный путь из текущего каталога диска C:. Использование второй формы при назначении первой является общим источником ошибок, включающих пути к файлам Windows.

Можно определить, является ли путь к файлу полным (то есть, если путь не зависит от текущего каталога и не изменяется при изменении текущего каталога), вызвав Path.IsPathFullyQualified метод. Обратите внимание, что такой путь может включать относительные сегменты каталога (. и ..) и все равно считаться полностью квалифицированным, если разрешенный путь всегда указывает на одно и то же местоположение.

В следующем примере показано различие между абсолютными и относительными путями. Предполагается, что каталог D:\FY2018\ существует и что вы не установили текущий каталог для D:\ в командной строке перед выполнением примера.

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;

public class Example2
{
    public static void Main(string[] args)
    {
        Console.WriteLine($"Current directory is '{Environment.CurrentDirectory}'");
        Console.WriteLine("Setting current directory to 'C:\\'");

        Directory.SetCurrentDirectory(@"C:\");
        string path = Path.GetFullPath(@"D:\FY2018");
        Console.WriteLine($"'D:\\FY2018' resolves to {path}");
        path = Path.GetFullPath(@"D:FY2018");
        Console.WriteLine($"'D:FY2018' resolves to {path}");

        Console.WriteLine("Setting current directory to 'D:\\Docs'");
        Directory.SetCurrentDirectory(@"D:\Docs");

        path = Path.GetFullPath(@"D:\FY2018");
        Console.WriteLine($"'D:\\FY2018' resolves to {path}");
        path = Path.GetFullPath(@"D:FY2018");

        // This will be "D:\Docs\FY2018" as it happens to match the drive of the current directory
        Console.WriteLine($"'D:FY2018' resolves to {path}");

        Console.WriteLine("Setting current directory to 'C:\\'");
        Directory.SetCurrentDirectory(@"C:\");

        path = Path.GetFullPath(@"D:\FY2018");
        Console.WriteLine($"'D:\\FY2018' resolves to {path}");

        // This will be either "D:\FY2018" or "D:\FY2018\FY2018" in the subprocess. In the sub process,
        // the command prompt set the current directory before launch of our application, which
        // sets a hidden environment variable that is considered.
        path = Path.GetFullPath(@"D:FY2018");
        Console.WriteLine($"'D:FY2018' resolves to {path}");

        if (args.Length < 1)
        {
            Console.WriteLine(@"Launching again, after setting current directory to D:\FY2018");
            Uri currentExe = new(Assembly.GetExecutingAssembly().Location, UriKind.Absolute);
            string commandLine = $"/C cd D:\\FY2018 & \"{currentExe.LocalPath}\" stop";
            ProcessStartInfo psi = new("cmd", commandLine); ;
            Process.Start(psi).WaitForExit();

            Console.WriteLine("Sub process returned:");
            path = Path.GetFullPath(@"D:\FY2018");
            Console.WriteLine($"'D:\\FY2018' resolves to {path}");
            path = Path.GetFullPath(@"D:FY2018");
            Console.WriteLine($"'D:FY2018' resolves to {path}");
        }
        Console.WriteLine("Press any key to continue... ");
        Console.ReadKey();
    }
}
// The example displays the following output:
//      Current directory is 'C:\Programs\file-paths'
//      Setting current directory to 'C:\'
//      'D:\FY2018' resolves to D:\FY2018
//      'D:FY2018' resolves to d:\FY2018
//      Setting current directory to 'D:\Docs'
//      'D:\FY2018' resolves to D:\FY2018
//      'D:FY2018' resolves to D:\Docs\FY2018
//      Setting current directory to 'C:\'
//      'D:\FY2018' resolves to D:\FY2018
//      'D:FY2018' resolves to d:\FY2018
//      Launching again, after setting current directory to D:\FY2018
//      Sub process returned:
//      'D:\FY2018' resolves to D:\FY2018
//      'D:FY2018' resolves to d:\FY2018
// The subprocess displays the following output:
//      Current directory is 'C:\'
//      Setting current directory to 'C:\'
//      'D:\FY2018' resolves to D:\FY2018
//      'D:FY2018' resolves to D:\FY2018\FY2018
//      Setting current directory to 'D:\Docs'
//      'D:\FY2018' resolves to D:\FY2018
//      'D:FY2018' resolves to D:\Docs\FY2018
//      Setting current directory to 'C:\'
//      'D:\FY2018' resolves to D:\FY2018
//      'D:FY2018' resolves to D:\FY2018\FY2018
Imports System.IO
Imports System.Reflection

Public Module Example2

    Public Sub Main(args() As String)
        Console.WriteLine($"Current directory is '{Environment.CurrentDirectory}'")
        Console.WriteLine("Setting current directory to 'C:\'")
        Directory.SetCurrentDirectory("C:\")

        Dim filePath As String = Path.GetFullPath("D:\FY2018")
        Console.WriteLine($"'D:\\FY2018' resolves to {filePath}")
        filePath = Path.GetFullPath("D:FY2018")
        Console.WriteLine($"'D:FY2018' resolves to {filePath}")

        Console.WriteLine("Setting current directory to 'D:\\Docs'")
        Directory.SetCurrentDirectory("D:\Docs")

        filePath = Path.GetFullPath("D:\FY2018")
        Console.WriteLine($"'D:\\FY2018' resolves to {filePath}")
        filePath = Path.GetFullPath("D:FY2018")

        ' This will be "D:\Docs\FY2018" as it happens to match the drive of the current directory
        Console.WriteLine($"'D:FY2018' resolves to {filePath}")

        Console.WriteLine("Setting current directory to 'C:\\'")
        Directory.SetCurrentDirectory("C:\")

        filePath = Path.GetFullPath("D:\FY2018")
        Console.WriteLine($"'D:\\FY2018' resolves to {filePath}")

        ' This will be either "D:\FY2018" or "D:\FY2018\FY2018" in the subprocess. In the sub process,
        ' the command prompt set the current directory before launch of our application, which
        ' sets a hidden environment variable that is considered.
        filePath = Path.GetFullPath("D:FY2018")
        Console.WriteLine($"'D:FY2018' resolves to {filePath}")

        If args.Length < 1 Then
            Console.WriteLine("Launching again, after setting current directory to D:\FY2018")
            Dim currentExe As New Uri(Assembly.GetExecutingAssembly().GetName().CodeBase, UriKind.Absolute)
            Dim commandLine As String = $"/C cd D:\FY2018 & ""{currentExe.LocalPath}"" stop"
            Dim psi As New ProcessStartInfo("cmd", commandLine)
            Process.Start(psi).WaitForExit()

            Console.WriteLine("Sub process returned:")
            filePath = Path.GetFullPath("D:\FY2018")
            Console.WriteLine($"'D:\\FY2018' resolves to {filePath}")
            filePath = Path.GetFullPath("D:FY2018")
            Console.WriteLine($"'D:FY2018' resolves to {filePath}")
        End If
        Console.WriteLine("Press any key to continue... ")
        Console.ReadKey()
    End Sub
End Module
' The example displays the following output:
'      Current directory is 'C:\Programs\file-paths'
'      Setting current directory to 'C:\'
'      'D:\FY2018' resolves to D:\FY2018
'      'D:FY2018' resolves to d:\FY2018
'      Setting current directory to 'D:\Docs'
'      'D:\FY2018' resolves to D:\FY2018
'      'D:FY2018' resolves to D:\Docs\FY2018
'      Setting current directory to 'C:\'
'      'D:\FY2018' resolves to D:\FY2018
'      'D:FY2018' resolves to d:\FY2018
'      Launching again, after setting current directory to D:\FY2018
'      Sub process returned:
'      'D:\FY2018' resolves to D:\FY2018
'      'D:FY2018' resolves to d:\FY2018
' The subprocess displays the following output:
'      Current directory is 'C:\'
'      Setting current directory to 'C:\'
'      'D:\FY2018' resolves to D:\FY2018
'      'D:FY2018' resolves to D:\FY2018\FY2018
'      Setting current directory to 'D:\Docs'
'      'D:\FY2018' resolves to D:\FY2018
'      'D:FY2018' resolves to D:\Docs\FY2018
'      Setting current directory to 'C:\'
'      'D:\FY2018' resolves to D:\FY2018
'      'D:FY2018' resolves to D:\FY2018\FY2018

UNC-пути

Пути универсального именования (UNC), используемые для доступа к сетевым ресурсам, имеют следующий формат:

  • Имя сервера или узла, предшествующее \\. Имя сервера может быть именем компьютера NetBIOS или IP-адресом или полным доменным именем (IPv4, а также версия 6 поддерживаются).
  • Имя общего ресурса, которое разделяется от имени узла с помощью \. Вместе сервер и имя общего ресурса составляют том.
  • Имя каталога. Символ разделителя каталогов отделяет подкаталоги в иерархически вложенных каталогах.
  • Необязательное имя файла. Символ разделителя каталогов разделяет путь к файлу и имя файла.

Ниже приведены некоторые примеры путей UNC:

Путь Описание
\\system07\C$\ Корневой каталог диска C: на system07.
\\Server2\Share\Test\Foo.txt Файл Foo.txt в папке Test на диске \\Server2\Share.

UNC-пути всегда должны быть полностью квалифицированными. Они могут включать относительные сегменты каталогов (. и ..), но они должны быть частью полного пути. Относительные пути можно использовать только, если присвоить UNC-пути букву диска.

Пути устройств DOS

Операционная система Windows имеет единую объектную модель, которая указывает на все ресурсы, включая файлы. Эти пути к объектам доступны из окна консоли и предоставляются на уровне Win32 через специальную папку символических ссылок, с которыми сопоставлены устаревшие пути DOS и UNC. Доступ к этой специальной папке осуществляется с помощью синтаксиса пути к устройству DOS, который является одним из следующих вариантов:

\\.\C:\Test\Foo.txt \\?\C:\Test\Foo.txt

Помимо идентификации диска по букве диска, можно определить том с помощью GUID тома. Это принимает форму:

\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\Test\Foo.txt \\?\Volume{b75e2c83-0000-0000-0000-602f00000000}\Test\Foo.txt

Замечание

Синтаксис пути к устройству DOS поддерживается в реализациях .NET, работающих в Windows, начиная с .NET Core 1.1 и .NET Framework 4.6.2.

Путь устройства DOS состоит из следующих компонентов:

  • Описатель пути устройства (\\.\ или \\?\), который определяет путь как путь к устройству DOS.

    Замечание

    \\?\ поддерживается во всех версиях .NET Core, .NET 5+ и в .NET Framework, начиная с версии 4.6.2.

  • Символьная ссылка на 'реальный' объект устройства (C: в случае имени диска или Volume{b75e2c83-0000-0000-0000-602f00000000} в случае GUID тома).

    Первый сегмент пути устройства DOS после описателя пути устройства идентифицирует том или диск. (Например, \\?\C:\ и \\.\BootPartition\.)

    Существует специальная ссылка для универсальных имен, которая называется, как ни странно, UNC. Рассмотрим пример.

    \\.\UNC\Server\Share\Test\Foo.txt \\?\UNC\Server\Share\Test\Foo.txt

    Для устройств UNCs сервер или часть общего ресурса формирует том. Например, в \\?\server1\utilities\\filecomparer\часть сервера/общего ресурса обозначена как server1\utilities. Это важно при вызове метода, например Path.GetFullPath(String, String), с относительными сегментами каталогов, поскольку невозможно выйти за пределы тома.

Пути к устройству DOS полностью соответствуют определению и не могут начинаться с относительного сегмента каталога (. или ..). Текущие каталоги никогда не вступают в их использование.

Пример. Способы ссылки на один и тот же файл

В следующем примере показано, как можно ссылаться на файл при использовании API в System.IO пространстве имен. Пример создает объект FileInfo и использует его свойства Name и Length для отображения имени файла и его длины.

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string[] filenames = {
            @"c:\temp\test-file.txt",
            @"\\127.0.0.1\c$\temp\test-file.txt",
            @"\\LOCALHOST\c$\temp\test-file.txt",
            @"\\.\c:\temp\test-file.txt",
            @"\\?\c:\temp\test-file.txt",
            @"\\.\UNC\LOCALHOST\c$\temp\test-file.txt" };

        foreach (string filename in filenames)
        {
            FileInfo fi = new(filename);
            Console.WriteLine($"file {fi.Name}: {fi.Length:N0} bytes");
        }
    }
}
// The example displays output like the following:
//      file test-file.txt: 22 bytes
//      file test-file.txt: 22 bytes
//      file test-file.txt: 22 bytes
//      file test-file.txt: 22 bytes
//      file test-file.txt: 22 bytes
//      file test-file.txt: 22 bytes
Imports System.IO

Module Program
    Sub Main()
        Dim filenames() As String = {
                "c:\temp\test-file.txt",
                "\\127.0.0.1\c$\temp\test-file.txt",
                "\\LOCALHOST\c$\temp\test-file.txt",
                "\\.\c:\temp\test-file.txt",
                "\\?\c:\temp\test-file.txt",
                "\\.\UNC\LOCALHOST\c$\temp\test-file.txt"}

        For Each filename In filenames
            Dim fi As New FileInfo(filename)
            Console.WriteLine($"file {fi.Name}: {fi.Length:N0} bytes")
        Next
    End Sub
End Module

Нормализация путей

Почти все пути, передаваемые в API Windows, нормализуются. Во время нормализации Windows выполняет следующие действия:

  • Определяет путь.
  • Применяет текущий каталог к частично квалифицированным (относительным) путям.
  • Канонизирует компоненты и разделители каталогов.
  • Оценивает относительные компоненты каталога (. для текущего каталога и .. родительского каталога).
  • Обрезает определенные символы.

Эта нормализация происходит неявно, но это можно сделать явным образом, вызвав Path.GetFullPath метод, который упаковывает вызов функции GetFullPathName(). Вы также можете вызвать функцию Windows GetFullPathName() непосредственно с помощью P/Invoke.

Определение пути

Первым шагом нормализации пути является определение типа пути. Пути делятся на одну из нескольких категорий:

  • Это пути к устройству; то есть они начинаются с двух разделителей и вопросительного знака или периода (\\? или \\.).
  • Это UNC-пути; то есть они начинаются с двух разделителей без вопросительного знака или периода.
  • Они являются полными путями DOS; то есть они начинаются с буквы диска, разделителя томов и разделителя компонентов (C:\).
  • Они обозначают устаревшее устройство (CON, LPT1).
  • Они относительны к корню текущего диска; то есть они начинаются с одного разделителя компонентов (\).
  • Они относительны к текущему каталогу указанного диска; то есть они начинаются с буквы диска, разделителя томов и без разделителя компонентов (C:).
  • Они относительны к текущему каталогу; то есть они начинаются с чего-либо другого (temp\testfile.txt).

Тип пути определяет, применяется ли текущий каталог каким-то образом. Он также определяет, что такое "корень" пути.

Обработка устаревших устройств

Если путь является устаревшим устройством DOS, например CON, COM1или LPT1, оно преобразуется в путь устройства путем подготовки \\.\ и возврата.

До Windows 11 путь, начинающийся с устаревшего имени устройства, всегда интерпретируется как устаревшее устройство методом Path.GetFullPath(String) . Например, путь к устройству DOS для CON.TXT\\.\CON, а путь к устройству DOS для COM1.TXT\file1.txt\\.\COM1. Так как это больше не применяется к Windows 11, укажите полный путь к устаревшему устройству DOS, например \\.\CON.

Применение текущего каталога

Если путь не является полным, Windows применяет к нему текущий каталог. Пути и устройства UNC не влияют на текущий каталог. Ни полный диск с разделителем C:\ не помогает.

Если путь начинается с одного разделителя компонентов, применяется диск из текущего каталога. Например, если путь к файлу — это \utilities, а текущий каталог — это C:\temp\, нормализация создаёт C:\utilities.

Если путь начинается с буквы диска, разделителя томов и без разделителя компонентов, применяется последний текущий набор каталогов из командной оболочки для указанного диска. Если последний текущий каталог не был задан, применяется только диск. Например, если путь к файлу D:sources, текущий каталог — C:\Documents\, а предыдущий текущий каталог на диске D: был D:\sources\, результат — D:\sources\sources. Эти относительные пути диска являются общим источником ошибок логики программы и скрипта. Предполагать, что путь, начинающийся с буквы и двоеточия, не является относительным, очевидно неправильно.

Если путь начинается не с разделителя, применяется текущий диск и текущий каталог. Например, если путь - это filecompare, а текущий каталог - C:\utilities\, результатом будет C:\utilities\filecompare\.

Это важно

Относительные пути опасны в многопоточных приложениях (т. е. в большинстве приложений), так как текущий каталог является параметром для каждого процесса. Любой поток может изменять текущий каталог в любое время. Начиная с .NET Core 2.1, можно вызвать Path.GetFullPath(String, String) метод, чтобы получить абсолютный путь от относительного пути и базового пути (текущего каталога), против которого требуется разрешить его.

Канонизировать разделители

Все косые черты (/) преобразуются в стандартный разделитель Windows, обратную косую черту (\). Если они присутствуют, то ряд косых черт, которые следуют за первыми двумя косыми чертами, будет свернут в одну косую черту.

Замечание

Начиная с .NET 8 в операционных системах на основе Unix, среда выполнения больше не преобразует символы обратной косой черты (\) в разделители каталогов (символы косой черты впереди /). Дополнительные сведения см. в разделе "Отображение обратной косой черты в путях файлов Unix".

Оценка относительных компонентов

По мере обработки пути вычисляются все компоненты или сегменты, состоящие из одного или двойного периода (. или ..) :

  • В течение одного периода текущий сегмент удаляется, так как он относится к текущему каталогу.

  • В течение двойного периода текущий сегмент и родительский сегмент удаляются, так как двойной период относится к родительскому каталогу.

    Родительские каталоги удаляются только в том случае, если они не выходят за пределы корня пути. Корень пути зависит от типа пути. Это диск (C:\) для путей DOS, сервер или общий доступ для UNCs (\\Server\Share), а также префикс пути устройства для путей устройства (\\?\ или \\.\).

Обрезать символы

Наряду с последовательностями разделителей и относительными сегментами, которые были удалены ранее, удаляются также некоторые дополнительные символы во время нормализации.

  • Если сегмент заканчивается одним периодом, этот период удаляется. (Сегмент одного или двойного периода нормализуется на предыдущем шаге. Сегмент из трех или более периодов не нормализован и фактически является допустимым именем файла или каталога.)

  • Если путь не заканчивается разделителем, удаляются все конечные периоды и пробелы (U+0020). Если последний сегмент является просто одним или двойным периодом, он попадает под правило относительных компонентов выше.

    Это правило означает, что вы можете создать имя каталога с конечным пробелом, добавив конечный разделитель после пробела.

    Это важно

    Вы никогда не должны создавать каталог или имя файла с конечным пробелом. Конечные пробелы могут затруднить или сделать невозможным доступ к каталогу, и приложения часто не удаются при попытке обрабатывать каталоги или файлы, имена которых включают конечные пробелы.

Пропустить нормализацию

Обычно любой путь, передаваемый в API Windows, передается функции GetFullPathName и нормализован. Существует одно важное исключение: путь устройства, начинающийся с вопросительного знака вместо периода. Если путь не начинается точно с \\?\ (обратите внимание на использование канонической обратной косой черты), он подвергается нормализации.

Почему вы хотите пропустить нормализацию? Существует три основных причины:

  1. Чтобы получить доступ к путям, которые обычно недоступны, но являются законными. Например, файл или каталог с именем hidden. невозможно получить доступ никаким другим способом.

  2. Чтобы повысить производительность, не выполняя нормализацию, если нормализация уже выполнена.

  3. Только в .NET Framework, чтобы пропустить MAX_PATH проверку длины пути, чтобы разрешить пути, превышающие 259 символов. Большинство API позволяют это, за исключением некоторых исключений.

Замечание

.NET Core и .NET 5+ обрабатывают длинные пути неявно и не выполняют MAX_PATH проверку. Проверка MAX_PATH применяется только к .NET Framework.

Пропуск нормализации и проверки максимального пути — это единственное различие между двумя синтаксисами пути устройства; В противном случае они идентичны. Будьте осторожны с пропуском нормализации, так как вы можете легко создавать пути, которые сложно обрабатывать обычными приложениями.

Пути, начинающиеся с \\?\ , по-прежнему нормализуются при явном передаче их функции GetFullPathName.

Вы можете передавать пути длиной более MAX_PATH символов в GetFullPathName при отсутствии \\?\. Он поддерживает произвольные пути длины до максимального размера строки, которую может обрабатывать Windows.

Регистр и файловая система Windows

Особенность файловой системы Windows, которую пользователи и разработчики других систем находят запутанной, заключается в том, что имена путей и каталогов нечувствительны к регистру. То есть имена каталогов и файлов отражают регистр строк, используемых при создании. Например, вызов метода

Directory.Create("TeStDiReCtOrY");
Directory.Create("TeStDiReCtOrY")

создает каталог с именем TeStDiReCtOrY. При переименовании каталога или файла, чтобы изменить его регистр, имя каталога или файла отражает регистр строки, используемой при переименовании. Например, следующий код переименовывает файл с именем test.txt в Test.txt:

using System.IO;

class Example3
{
    static void Main()
    {
        var fi = new FileInfo(@".\test.txt");
        fi.MoveTo(@".\Test.txt");
    }
}
Imports System.IO

Module Example3
    Public Sub Main()
        Dim fi As New FileInfo(".\test.txt")
        fi.MoveTo(".\Test.txt")
    End Sub
End Module

Однако сравнение имен каталогов и файлов не учитывает регистр. Если вы ищете файл с именем "test.txt", API файловой системы .NET игнорируют регистр при сравнении. "Test.txt", "TEST.TXT", "test.TXT", а также любое другое сочетание прописных и строчных букв будет соответствовать "test.txt".