Dlaczego Path.Combine nieprawidłowo łączy nazwy plików rozpoczynające się od Path.DirectorySeparatorChar?

185

Z okna natychmiastowego w Visual Studio:

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

Wydaje się, że oba powinny być takie same.

Stara FileSystemObject.BuildPath () nie działała w ten sposób ...

Kris Erickson
źródło
@Joe, głupi ma rację! Muszę też zaznaczyć, że równoważna funkcja działa dobrze w Node.JS ... Kręcąc głową w Microsoft ...
NH.
2
@zwcloud Dla platformy .NET Core / Standard Path.Combine()służy głównie do kompatybilności wstecznej (z istniejącym zachowaniem). Lepiej byłoby użyć Path.Join(): „W przeciwieństwie do metody Łączenia, metoda łączenia nie próbuje zrootować zwróconej ścieżki. (To znaczy, jeśli ścieżka2 jest ścieżką bezwzględną, metoda łączenia nie odrzuca ścieżki 1 i zwraca ścieżkę 2 jako Łączenie metoda robi.) ”
Stajs

Odpowiedzi:

205

Jest to rodzaj filozoficznego pytania (na które być może tylko Microsoft może naprawdę odpowiedzieć), ponieważ robi dokładnie to, co mówi dokumentacja.

System.IO.Path.Combine

„Jeśli ścieżka2 zawiera ścieżkę bezwzględną, ta metoda zwraca ścieżkę 2.”

Oto faktyczna metoda Combine ze źródła .NET. Możesz zobaczyć, że wywołuje on CombineNoChecks , który następnie wywołuje IsPathRooted na path2 i zwraca tę ścieżkę, jeśli tak:

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;
}

Nie wiem, jakie jest uzasadnienie. Myślę, że rozwiązaniem jest usunięcie (lub przycięcie) DirectorySeparatorChar od początku drugiej ścieżki; może napisz własną metodę Combine, która to robi, a następnie wywołuje Path.Combine ().

Ryan Lundy
źródło
Patrząc na zdemontowany kod (sprawdź mój post), masz rację.
Gulzar Nazim
7
Sądzę, że działa w ten sposób, aby umożliwić łatwy dostęp do algorytmu „bieżący działający katalog”.
BCS,
Wygląda na to, że działa jak sekwencja cd (component)z wiersza poleceń. Brzmi dla mnie rozsądnie.
Adrian Ratnapala
11
Używam tego przycinania, aby uzyskać pożądany ciąg efektu strFilePath = Path.Combine (basePath, otherPath.TrimStart (new char [] {'\\', '/'}));
Matthew Lock
3
Path.Combine
Zmieniłem
23

To jest zdemontowany kod z .NET Reflector dla metody Path.Combine. Sprawdź funkcję IsPathRooted. Jeśli druga ścieżka jest zrootowana (zaczyna się od DirectorySeparatorChar), zwróć drugą ścieżkę taką, jaka jest.

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;
}
Gulzar Nazim
źródło
23

Chciałem rozwiązać ten problem:

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);

Oczywiście wszystkie ścieżki 1-9 powinny zawierać równoważny ciąg na końcu. Oto metoda PathCombine, którą wymyśliłem:

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);
}

Myślę też, że to dość denerwujące, że ta obsługa łańcuchów musi być wykonywana ręcznie, i byłbym zainteresowany przyczyną tego.

anhoppe
źródło
19

Moim zdaniem jest to błąd. Problem polega na tym, że istnieją dwa różne rodzaje ścieżek „absolutnych”. Ścieżka „d: \ mydir \ myfile.txt” jest bezwzględna, ścieżka „\ mydir \ myfile.txt” również jest uważana za „bezwzględną”, mimo że brakuje jej litery dysku. Moim zdaniem poprawnym działaniem byłoby wstawienie litery dysku z pierwszej ścieżki, gdy druga ścieżka zaczyna się od separatora katalogów (i nie jest ścieżką UNC). Poleciłbym napisać własną funkcję opakowania pomocnika, która zachowa się tak, jak chcesz, jeśli jej potrzebujesz.

Klin
źródło
7
Jest zgodny ze specyfikacją, ale nie tego też bym się spodziewał.
dthrasher
@Jake To nie omija poprawki; to kilka osób zastanawiających się długo, jak coś zrobić, a następnie trzymających się tego, na co się zgodzą. Zwróć także uwagę na różnicę między frameworkiem .Net (biblioteką zawierającą Path.Combine) a językiem C #.
Grault
9

Z MSDN :

Jeśli jedna z określonych ścieżek jest łańcuchem o zerowej długości, ta metoda zwraca drugą ścieżkę. Jeśli ścieżka2 zawiera ścieżkę bezwzględną, ta metoda zwraca ścieżkę2.

W twoim przykładzie ścieżka 2 jest absolutna.

nickd
źródło
7

Zgodnie z radą Christiana Grausa w blogu „Things I Hate about Microsoft” zatytułowanym „ Path.Combine jest zasadniczo bezużyteczny. Oto moje rozwiązanie:

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);
    }
}

Niektórzy radzą, aby przestrzenie nazw kolidowały, ... Poszedłem Pathytrochę, aby uniknąć kolizji przestrzeni nazw System.IO.Path.

Edycja : Dodano sprawdzanie parametrów zerowych

ergohack
źródło
4

Ten kod powinien załatwić sprawę:

        string strFinalPath = string.Empty;
        string normalizedFirstPath = Path1.TrimEnd(new char[] { '\\' });
        string normalizedSecondPath = Path2.TrimStart(new char[] { '\\' });
        strFinalPath =  Path.Combine(normalizedFirstPath, normalizedSecondPath);
        return strFinalPath;
Król
źródło
4

Nie znając faktycznych szczegółów, domyślam się, że próbuje się połączyć, jakbyś mógł dołączyć względne identyfikatory URI. Na przykład:

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

Oznacza to, że łącząc ścieżkę z poprzednim ukośnikiem, faktycznie łączysz jedną bazę z drugą, w którym to przypadku druga ma pierwszeństwo.

Elarson
źródło
Myślę, że przednie ukośniki powinny zostać wyjaśnione. Co to ma wspólnego z platformą .NET?
Peter Mortensen
3

Powód:

Twój drugi adres URL jest uważany za ścieżkę bezwzględną. CombineMetoda zwróci ostatnią ścieżkę tylko wtedy, gdy ostatnia ścieżka jest ścieżką bezwzględną.

Rozwiązanie: Wystarczy usunąć początkowy ukośnik /z drugiej ścieżki ( /SecondPathdo SecondPath). Następnie działa tak, jak wyjątek.

Amir Hossein Ahmadi
źródło
3

Ma to w pewnym sensie sens, biorąc pod uwagę, w jaki sposób (względne) ścieżki są zwykle traktowane:

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

Prawdziwe pytanie brzmi: dlaczego ścieżki, które zaczynają się "\", są uważane za „zakorzenione”? To również było dla mnie nowe, ale działa tak w systemie Windows :

new FileInfo("\windows"); // FullName = C:\Windows, Exists = True
new FileInfo("windows"); // FullName = C:\Users\Foo.Bar\Windows, Exists = False
marsze
źródło
1

Jeśli chcesz połączyć obie ścieżki bez utraty żadnej ścieżki, możesz użyć tego:

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

Lub ze zmiennymi:

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

Oba przypadki zwracają „C: \ test \ test”.

Najpierw oceniam, czy ścieżka 2 zaczyna się od / i czy to prawda, zwracam ścieżkę 2 bez pierwszego znaku. W przeciwnym razie zwróć pełną ścieżkę2.

Ferri
źródło
1
Prawdopodobnie bezpieczniej jest zastąpić == @"\"czek Path.IsRooted()rozmową, ponieważ "\"nie jest to jedyna postać, którą należy uwzględnić.
rumblefx0
0

Te dwie metody powinny uchronić cię przed przypadkowym połączeniem dwóch ciągów, które zawierają ogranicznik.

    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);
    }
Don Rolling
źródło
0

To \ oznacza „katalog główny bieżącego dysku”. W twoim przykładzie oznacza to folder „testowy” w katalogu głównym bieżącego dysku. Może to być więc równe „c: \ test”.

Estevez
źródło
0

Usuń początkowy ukośnik („\”) z drugiego parametru (ścieżka2) Path.Combine.

shanmuga raja
źródło
Pytanie nie zadaje tego.
LarsTech,
0

Użyłem funkcji agregującej, aby wymusić połączenie ścieżek, jak poniżej:

public class MyPath    
{
    public static string ForceCombine(params string[] paths)
    {
        return paths.Aggregate((x, y) => Path.Combine(x, y.TrimStart('\\')));
    }
}
Laz Ziya
źródło
0

Jak wspomniał Ryan, robi dokładnie to, co mówi dokumentacja.

Od czasów DOS rozróżnia się bieżący dysk i bieżącą ścieżkę. \jest ścieżką katalogu głównego, ale dla AKTUALNEGO DYSKU.

Dla każdego „ dysku ” istnieje osobna „ bieżąca ścieżka ”. Jeśli zmienisz dysk za pomocącd D: , nie zmienisz bieżącej ścieżki na D:\, ale na: „D: \ cokolwiek \ było \ ostatnią \ ścieżką \ dostępną \ na \ tym \ dysku” ...

Tak więc w systemie Windows literał @"\x"oznacza: „CURRENTDISK: \ x”. Dlatego Path.Combine(@"C:\x", @"\y")jako drugi parametr ma ścieżkę katalogu głównego, a nie krewnego, choć nie na znanym dysku ... A ponieważ nie wiadomo, który może być «bieżącym dyskiem», zwraca python "\\y".

>cd C:
>cd \mydironC\apath
>cd D:
>cd \mydironD\bpath
>cd C:
>cd
>C:\mydironC\apath
ilias iliadis
źródło