Чому 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.