Perché Path.Combine non concatena correttamente i nomi di file che iniziano con Path.DirectorySeparatorChar?


185

Dalla finestra immediata in Visual Studio:

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

Sembra che dovrebbero essere entrambi uguali.

Il vecchio FileSystemObject.BuildPath () non funzionava in questo modo ...



@Joe, stupido ha ragione! Inoltre, devo sottolineare che la funzione equivalente funziona perfettamente in Node.JS ... Scuotendo la testa a Microsoft ...
NH.

2
@zwcloud Per .NET Core / Standard, Path.Combine()è principalmente per la compatibilità con le versioni precedenti (con il comportamento esistente). Faresti meglio a usare Path.Join(): "A differenza del metodo Combine, il metodo Join non tenta di eseguire il root del percorso restituito. (Cioè, se path2 è un percorso assoluto, il metodo Join non scarta path1 e restituisce path2 come Combina metodo.) "
Stajs,

Risposte:


205

Questa è una specie di domanda filosofica (a cui forse solo Microsoft può veramente rispondere), dal momento che sta facendo esattamente ciò che dice la documentazione.

System.IO.Path.Combine

"Se path2 contiene un percorso assoluto, questo metodo restituisce path2."

Ecco l'attuale metodo Combine dall'origine .NET. Puoi vedere che chiama CombineNoChecks , che quindi chiama IsPathRooted su path2 e restituisce quel percorso in tal caso:

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

Non so quale sia la logica. Immagino che la soluzione sia rimuovere (o tagliare) DirectorySeparatorChar dall'inizio del secondo percorso; forse scrivi il tuo metodo Combine che lo fa e poi chiama Path.Combine ().


Guardando il codice smontato (controlla il mio post), hai ragione.
Gulzar Nazim,

7
Immagino che funzioni in questo modo per consentire un facile accesso all'algoritmo "attuale directory di lavoro".
BCS,

Sembra funzionare come fare una sequenza cd (component)dalla riga di comando. Mi sembra ragionevole.
Adrian Ratnapala,

11
Uso questo trim per ottenere la stringa di effetti desiderata strFilePath = Path.Combine (basePath, otherPath.TrimStart (new char [] {'\\', '/'}));
Matthew Lock,

3
Ho cambiato il mio codice di lavoro Path.Combinesolo per sicurezza, ma poi si è rotto ... È così stupido :)
sotn

23

Questo è il codice disassemblato dal metodo .NET Reflector per Path.Combine. Controlla la funzione IsPathRooted. Se il secondo percorso è rootato (inizia con un DirectorySeparatorChar), restituisce il secondo percorso così com'è.

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

Volevo risolvere questo problema:

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

Naturalmente, tutti i percorsi 1-9 dovrebbero contenere una stringa equivalente alla fine. Ecco il metodo PathCombine che ho ideato:

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

Penso anche che sia abbastanza fastidioso che questa gestione delle stringhe debba essere eseguita manualmente e sarei interessato al motivo dietro questo.


19

Secondo me questo è un bug. Il problema è che esistono due diversi tipi di percorsi "assoluti". Il percorso "d: \ mydir \ myfile.txt" è assoluto, anche il percorso "\ mydir \ myfile.txt" è considerato "assoluto" anche se manca la lettera dell'unità. Il comportamento corretto, a mio avviso, sarebbe quello di anteporre la lettera di unità dal primo percorso quando il secondo percorso inizia con il separatore di directory (e non è un percorso UNC). Consiglierei di scrivere la tua funzione di helper wrapper che ha il comportamento che desideri se ne hai bisogno.


7
Corrisponde alle specifiche, ma non è nemmeno quello che mi sarei aspettato.
Dthrasher,

@Jake Questo non evita un bugfix; sono diverse le persone che pensano a lungo e intensamente su come fare qualcosa, e poi attenersi a qualsiasi cosa concordino. Inoltre, nota la differenza tra il framework .Net (una libreria che contiene Path.Combine) e il linguaggio C #.
Grault

9

Da MSDN :

Se uno dei percorsi specificati è una stringa di lunghezza zero, questo metodo restituisce l'altro percorso. Se path2 contiene un percorso assoluto, questo metodo restituisce path2.

Nel tuo esempio, path2 è assoluto.


7

Seguendo i consigli di Christian Graus nel suo blog "Things I Hate about Microsoft" intitolato " Path.Combine è essenzialmente inutile ", ecco la mia soluzione:

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

Alcuni suggeriscono che gli spazi dei nomi dovrebbero scontrarsi, ... Sono andato con Pathy, come minimo, e per evitare la collisione dello spazio dei nomi System.IO.Path.

Modifica : aggiunti controlli dei parametri null


4

Questo codice dovrebbe fare il trucco:

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

4

Non conoscendo i dettagli effettivi, la mia ipotesi è che tenta di unirsi come se potessi unirti a URI relativi. Per esempio:

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

Ciò significa che quando si unisce un percorso con una barra precedente, in realtà si unisce una base a un'altra, nel qual caso la seconda ha la precedenza.


Penso che le barre in avanti dovrebbero essere spiegate. Inoltre, cosa c'entra questo con .NET?
Peter Mortensen,

3

Motivo:

Il tuo secondo URL è considerato un percorso assoluto, il Combinemetodo restituirà l'ultimo percorso solo se l'ultimo percorso è un percorso assoluto.

Soluzione: è sufficiente rimuovere la barra iniziale /del secondo percorso ( /SecondPathverso SecondPath). Quindi funziona come hai escluso.


3

Questo in realtà ha senso, in qualche modo, considerando come i percorsi (relativi) di solito vengono trattati:

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

La vera domanda è: perché i percorsi, che iniziano con "\", sono considerati "radicati"? Anche per me era una novità, ma funziona così su Windows :

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

1

Se si desidera combinare entrambi i percorsi senza perdere alcun percorso, è possibile utilizzare questo:

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

O con variabili:

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

Entrambi i casi restituiscono "C: \ test \ test".

Innanzitutto, valuto se Path2 inizia con / e se è vero, restituisco Path2 senza il primo carattere. In caso contrario, restituisce il Path2 completo.


1
Probabilmente è più sicuro sostituire l' == @"\"assegno con una Path.IsRooted()chiamata poiché "\"non è l'unico personaggio di cui tenere conto.
rumblefx0,

0

Questi due metodi dovrebbero salvarti dall'unione accidentale di due stringhe che contengono entrambe il delimitatore.

    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

Questo \ significa "la directory principale dell'unità corrente". Nel tuo esempio indica la cartella "test" nella directory principale dell'unità corrente. Quindi, questo può essere uguale a "c: \ test".


0

Rimuovere la barra iniziale ('\') nel secondo parametro (path2) di Path.Combine.


La domanda non sta ponendo questo.
LarsTech,

0

Ho usato la funzione aggregata per forzare i percorsi combinati come di seguito:

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

0

Come menzionato da Ryan, sta facendo esattamente ciò che dice la documentazione.

Dai tempi DOS, si distinguono il disco corrente e il percorso corrente. \è il percorso principale, ma per il DISCO CORRENTE.

Per ogni " disco " esiste un " percorso corrente " separato . Se si modifica il disco utilizzando cd D:non modificare il percorso corrente in D:\, ma in: "D: \ qualunque cosa \ era \ l'ultimo \ percorso \ accesso \ su \ questo \ disco" ...

Quindi, in Windows, un letterale @"\x"significa: "CURRENTDISK: \ x". Quindi Path.Combine(@"C:\x", @"\y")ha come secondo parametro un percorso radice, non un relativo, sebbene non in un disco noto ... E poiché non si sa quale possa essere il «disco corrente», ritorna Python "\\y".

>cd C:
>cd \mydironC\apath
>cd D:
>cd \mydironD\bpath
>cd C:
>cd
>C:\mydironC\apath
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.