Чому Path.Combine неправильно об'єднує імена файлів, які починаються з Path.DirectorySeparatorChar?


186

З негайного вікна в Visual Studio:

> Path.Combine(@"C:\x", "y")
"C:\\x\\y"
> Path.Combine(@"C:\x", @"\y")
"\\y"

Здається, що вони повинні бути однаковими.

Стара FileSystemObject.BuildPath () не працювала таким чином ...



@ Джо, дурний прав! Також я мушу зазначити, що еквівалентна функція працює чудово в Node.JS ... Похитавши головою в Microsoft ...
NH.

2
@zwcloud Для .NET Core / Standard - Path.Combine()це головним чином для зворотної сумісності (з існуючою поведінкою). Вам краще використовувати Path.Join(): "На відміну від методу" Комбінувати ", метод" Join "не намагається викорінити повернутий шлях. (Тобто, якщо path2 є абсолютним шляхом, метод Join не відкидає path1 та return path2 як Combine метод.) "
Stajs

Відповіді:


205

Це своєрідне філософське питання (на яке, можливо, лише Microsoft може по-справжньому відповісти), оскільки він робить саме те, що йдеться в документації.

System.IO.Path.Combine

"Якщо path2 містить абсолютний шлях, цей метод повертає path2."

Ось власне метод комбінування з джерела .NET. Ви можете бачити, що він викликає CombineNoChecks , який потім викликає IsPathRooted на path2 і повертає цей шлях, якщо так:

public static String Combine(String path1, String path2) {
    if (path1==null || path2==null)
        throw new ArgumentNullException((path1==null) ? "path1" : "path2");
    Contract.EndContractBlock();
    CheckInvalidPathChars(path1);
    CheckInvalidPathChars(path2);

    return CombineNoChecks(path1, path2);
}

internal static string CombineNoChecks(string path1, string path2)
{
    if (path2.Length == 0)
        return path1;

    if (path1.Length == 0)
        return path2;

    if (IsPathRooted(path2))
        return path2;

    char ch = path1[path1.Length - 1];
    if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar &&
            ch != VolumeSeparatorChar) 
        return path1 + DirectorySeparatorCharAsString + path2;
    return path1 + path2;
}

Я не знаю, що це за обгрунтування. Я думаю, що рішення полягає в тому, щоб зняти (або обрізати) DirectorySeparatorChar з початку другого шляху; можливо, напишіть свій власний метод Combine, який це робить, а потім викликає Path.Combine ().


Дивлячись на розібраний код (перевірте мій пост), ви певним чином маєте рацію.
Гульзар Назим

7
Я б припустив, що це працює таким чином, щоб дозволити легкий доступ до алгоритму "поточного робочого режиму".
BCS

Здається, це працює як виконання послідовності cd (component)з командного рядка. Звучить мені розумно.
Адріан Ратнапала

11
Я використовую цю обробку, щоб отримати бажаний рядок ефекту strFilePath = Path.Combine (basePath, otherPath.TrimStart (новий char [] {'\\', '/'}));
Метью Лок

3
Я змінив свій робочий код Path.Combineпросто на безпеку, але потім він зламався. Це так
дурно

23

Це розібраний код від .NET Reflector для методу Path.Combine. Перевірте функцію IsPathRooted. Якщо другий шлях укорінений (починається з DirectorySeparatorChar), поверніть другий шлях таким, яким він є.

public static string Combine(string path1, string path2)
{
    if ((path1 == null) || (path2 == null))
    {
        throw new ArgumentNullException((path1 == null) ? "path1" : "path2");
    }
    CheckInvalidPathChars(path1);
    CheckInvalidPathChars(path2);
    if (path2.Length == 0)
    {
        return path1;
    }
    if (path1.Length == 0)
    {
        return path2;
    }
    if (IsPathRooted(path2))
    {
        return path2;
    }
    char ch = path1[path1.Length - 1];
    if (((ch != DirectorySeparatorChar) &&
         (ch != AltDirectorySeparatorChar)) &&
         (ch != VolumeSeparatorChar))
    {
        return (path1 + DirectorySeparatorChar + path2);
    }
    return (path1 + path2);
}


public static bool IsPathRooted(string path)
{
    if (path != null)
    {
        CheckInvalidPathChars(path);
        int length = path.Length;
        if (
              (
                  (length >= 1) &&
                  (
                      (path[0] == DirectorySeparatorChar) ||
                      (path[0] == AltDirectorySeparatorChar)
                  )
              )

              ||

              ((length >= 2) &&
              (path[1] == VolumeSeparatorChar))
           )
        {
            return true;
        }
    }
    return false;
}

23

Я хотів вирішити цю проблему:

string sample1 = "configuration/config.xml";
string sample2 = "/configuration/config.xml";
string sample3 = "\\configuration/config.xml";

string dir1 = "c:\\temp";
string dir2 = "c:\\temp\\";
string dir3 = "c:\\temp/";

string path1 = PathCombine(dir1, sample1);
string path2 = PathCombine(dir1, sample2);
string path3 = PathCombine(dir1, sample3);

string path4 = PathCombine(dir2, sample1);
string path5 = PathCombine(dir2, sample2);
string path6 = PathCombine(dir2, sample3);

string path7 = PathCombine(dir3, sample1);
string path8 = PathCombine(dir3, sample2);
string path9 = PathCombine(dir3, sample3);

Звичайно, усі шляхи 1-9 повинні містити еквівалентний рядок у підсумку. Ось метод PathCombine, який я придумав:

private string PathCombine(string path1, string path2)
{
    if (Path.IsPathRooted(path2))
    {
        path2 = path2.TrimStart(Path.DirectorySeparatorChar);
        path2 = path2.TrimStart(Path.AltDirectorySeparatorChar);
    }

    return Path.Combine(path1, path2);
}

Я також думаю, що це дуже прикро, що цю обробку рядків потрібно проводити вручну, і мене зацікавить причина цього.


19

На мою думку це помилка. Проблема полягає в тому, що існує два різних типи "абсолютних" шляхів. Шлях "d: \ mydir \ myfile.txt" є абсолютним, шлях "\ mydir \ myfile.txt" також вважається "абсолютним", хоча у ньому відсутня літера диска. Правильною поведінкою, на мою думку, було б додати букву диска з першого шляху, коли другий шлях починається з роздільника каталогів (і це не шлях UNC). Я рекомендую написати власну функцію обгортки, яка потрібна вам, якщо вам це потрібно.


7
Він відповідає специфікації, але це не те, що я б і очікував.
dthrasher

@Jake Це не дозволяє уникнути помилок; це кілька людей, які довго і важко думають, як щось зробити, а потім дотримуються того, про що вони домовляться. Також зверніть увагу на різницю між .Net Framework (бібліотекою, яка містить Path.Combine) та мовою C #.
Грауль

9

Від MSDN :

Якщо один із зазначених шляхів є рядком нульової довжини, цей метод повертає інший шлях. Якщо path2 містить абсолютний шлях, цей метод повертає path2.

У вашому прикладі path2 є абсолютним.


7

Дотримуючись порад Крістіана Грауса у своєму блозі "Речі, які я ненавиджу щодо Microsoft" під назвою " Path.Combine є по суті марним ". Ось моє рішення:

public static class Pathy
{
    public static string Combine(string path1, string path2)
    {
        if (path1 == null) return path2
        else if (path2 == null) return path1
        else return path1.Trim().TrimEnd(System.IO.Path.DirectorySeparatorChar)
           + System.IO.Path.DirectorySeparatorChar
           + path2.Trim().TrimStart(System.IO.Path.DirectorySeparatorChar);
    }

    public static string Combine(string path1, string path2, string path3)
    {
        return Combine(Combine(path1, path2), path3);
    }
}

Деякі радять, що простори імен повинні стикатися, ... я пішов Pathy, як незначне, і щоб уникнути зіткнення з простором імен System.IO.Path.

Редагувати : Додано перевірку нульових параметрів


4

Цей код повинен зробити фокус:

        string strFinalPath = string.Empty;
        string normalizedFirstPath = Path1.TrimEnd(new char[] { '\\' });
        string normalizedSecondPath = Path2.TrimStart(new char[] { '\\' });
        strFinalPath =  Path.Combine(normalizedFirstPath, normalizedSecondPath);
        return strFinalPath;

4

Не знаючи фактичних деталей, я здогадуюсь, що він робить спробу приєднатися так, як ти можеш приєднатися до відносних URI. Наприклад:

urljoin('/some/abs/path', '../other') = '/some/abs/other'

Це означає, що коли ви приєднуєтесь до шляху з попередньою косою рисою, ви насправді приєднуєте одну базу до іншої, і в цьому випадку друга має перевагу.


Я думаю, що передні нахили слід пояснити. Також, що це стосується .NET?
Пітер Мортенсен

3

Причина:

Ваша друга URL-адреса вважається абсолютним шляхом, Combineметод поверне останній шлях лише в тому випадку, якщо останній шлях є абсолютним шляхом.

Рішення. Просто видаліть початкову косу рису /другого шляху ( /SecondPathдо SecondPath). Тоді воно працює, як ви виключили.


3

Це фактично має сенс, певним чином, враховуючи, як (відносні) шляхи трактуються зазвичай:

string GetFullPath(string path)
{
     string baseDir = @"C:\Users\Foo.Bar";
     return Path.Combine(baseDir, path);
}

// Get full path for RELATIVE file path
GetFullPath("file.txt"); // = C:\Users\Foo.Bar\file.txt

// Get full path for ROOTED file path
GetFullPath(@"C:\Temp\file.txt"); // = C:\Temp\file.txt

Справжнє запитання: Чому шляхи, які починаються з "\", вважаються "вкоріненими"? Це було для мене теж новим, але воно працює так у Windows :

new FileInfo("\windows"); // FullName = C:\Windows, Exists = True
new FileInfo("windows"); // FullName = C:\Users\Foo.Bar\Windows, Exists = False

1

Якщо ви хочете поєднати обидва шляхи, не втрачаючи жодного шляху, ви можете скористатися цим:

?Path.Combine(@"C:\test", @"\test".Substring(0, 1) == @"\" ? @"\test".Substring(1, @"\test".Length - 1) : @"\test");

Або зі змінними:

string Path1 = @"C:\Test";
string Path2 = @"\test";
string FullPath = Path.Combine(Path1, Path2.IsRooted() ? Path2.Substring(1, Path2.Length - 1) : Path2);

Обидва випадки повертають "C: \ test \ test".

По-перше, я оцінюю, чи починається Path2 з / і якщо це правда, поверніть Path2 без першого символу. В іншому випадку поверніть повний Шлях2.


1
Це, ймовірно, безпечніше замінити == @"\"чек на Path.IsRooted()дзвінок, оскільки "\"це не єдиний персонаж, який потрібно врахувати.
rumblefx0

0

Ці два способи повинні врятувати вас від випадкового з'єднання двох рядків, які в обох мають роздільник.

    public static string Combine(string x, string y, char delimiter) {
        return $"{ x.TrimEnd(delimiter) }{ delimiter }{ y.TrimStart(delimiter) }";
    }

    public static string Combine(string[] xs, char delimiter) {
        if (xs.Length < 1) return string.Empty;
        if (xs.Length == 1) return xs[0];
        var x = Combine(xs[0], xs[1], delimiter);
        if (xs.Length == 2) return x;
        var ys = new List<string>();
        ys.Add(x);
        ys.AddRange(xs.Skip(2).ToList());
        return Combine(ys.ToArray(), delimiter);
    }

0

Це \ означає "кореневий каталог поточного диска". У вашому прикладі це означає "тестову" папку в кореневому каталозі поточного диска. Отже, це може дорівнювати "c: \ test".


0

Видаліть початкову косу рису ('\') у другому параметрі (path2) Path.Combine.


Питання цього не задається.
LarsTech

0

Я використовував функцію сукупності, щоб змусити шляхи поєднуватись як нижче:

public class MyPath    
{
    public static string ForceCombine(params string[] paths)
    {
        return paths.Aggregate((x, y) => Path.Combine(x, y.TrimStart('\\')));
    }
}

0

Як згадує Райан, він робить саме те, що написано в документації.

Від часів DOS розрізняють поточний диск та поточний шлях. \- кореневий шлях, але для СУЧАСНОГО ДИСКУ.

Для кожного " диска " існує окремий " поточний шлях ". Якщо ви зміните диск за допомогою, cd D:ви не змінюєте поточний шлях на D:\, а на: "D: \ що б \ було \ \ \ останнє \ шлях \ доступ \ на \ цей \ диск" ...

Так, у вікнах буквальне @"\x"означає: "СУЧАСНИЙ ДИСК: \ х". Отже Path.Combine(@"C:\x", @"\y"), як другий параметр є кореневий шлях, а не відносний, хоча і не на відомому диску ... А оскільки невідомо, який може бути «поточний диск», python повертається "\\y".

>cd C:
>cd \mydironC\apath
>cd D:
>cd \mydironD\bpath
>cd C:
>cd
>C:\mydironC\apath
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.