Por que o Path.Combine não concatena apropriadamente nomes de arquivos que começam com Path.DirectorySeparatorChar?

A partir da janela imediata no Visual Studio:

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

Parece que ambos deveriam ser iguais.

O antigo FileSystemObject.BuildPath () não funcionava assim …

Essa é uma questão filosófica (que talvez apenas a Microsoft possa realmente responder), já que está fazendo exatamente o que a documentação diz.

System.IO.Path.Combine

“Se path2 contiver um caminho absoluto, esse método retornará path2.”

Aqui está o método Combine real da fonte .NET. Você pode ver que ele chama CombineNoChecks , que chama IsPathRooted no caminho2 e retorna esse caminho em caso afirmativo.

Eu não sei qual é a razão. Eu acho que a solução é retirar (ou Trim) DirectorySeparatorChar desde o início do segundo caminho; talvez escreva seu próprio método Combine que faz isso e, em seguida, chama Path.Combine ().

Este é o código desmontado do .NET Reflector for Path.Combine. Verifique a function IsPathRooted. Se o segundo caminho estiver enraizado (inicia com um DirectorySeparatorChar), retorne o segundo caminho como está.

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

Ok, já uma longa lista de respostas, aqui está a minha 😉

Eu queria resolver esse 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, todos os pacotes 1-9 devem conter uma string equivalente no final. Aqui está o método PathCombine que eu criei:

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

Eu também acho que é muito chato que essa manipulação de string tenha que ser feita manualmente, eu estaria interessado na razão por trás disso.

Na minha opinião, isso é um erro. O problema é que existem dois tipos diferentes de caminhos “absolutos”. O caminho “d: \ mydir \ myfile.txt” é absoluto, o caminho “\ mydir \ myfile.txt” também é considerado “absoluto” mesmo que esteja faltando a letra da unidade. O comportamento correto, na minha opinião, seria prefixar a letra da unidade do primeiro caminho quando o segundo caminho começa com o separador de diretório (e não é um caminho UNC). Eu recomendaria escrever sua própria function de wrapper auxiliar, que tem o comportamento desejado se você precisar.

Do MSDN :

Se um dos caminhos especificados for uma cadeia de comprimento zero, esse método retornará o outro caminho. Se path2 contiver um caminho absoluto, esse método retornará path2.

No seu exemplo, path2 é absoluto.

Não sabendo os detalhes reais, meu palpite é que ele faz uma tentativa de entrar como se você pudesse se juntar a URIs relativas. Por exemplo:

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

Isso significa que, quando você une um caminho com uma barra anterior, você está realmente unindo uma base a outra e, nesse caso, a segunda obtém precedência.

Seguindo o conselho de Christian Graus em seu blog “Things I Hate about Microsoft” chamado ” Path.Combine é essencialmente inútil “, aqui está a minha solução:

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

Alguns aconselham que os namespaces colidam, … Eu fui com o Pathy , como um leve, e para evitar a colisão de namespace com o System.IO.Path .

Editar : Adicionadas verificações de parâmetro nulo

Este código deve fazer o truque:

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

Isso significa “o diretório raiz da unidade atual”. No seu exemplo, significa a pasta “test” no diretório raiz da unidade atual. Então, isso pode ser igual a “c: \ test”

Se você quiser combinar os dois caminhos sem perder nenhum caminho, você pode usar isto:

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

Ou com variables:

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

Ambos os casos retornam “C: \ test \ test”.

Primeiro, eu avalio se o Path2 inicia com / e, se for verdade, retorne o Path2 sem o primeiro caractere. Caso contrário, retorne o Path2 completo.

Isso realmente faz sentido, de alguma forma, considerando como os caminhos (relativos) são tratados normalmente:

 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 

A verdadeira questão é, por que os caminhos que começam com "\" são considerados “enraizados”. Isso era novidade para mim, mas funciona assim no Windows :

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

Esses dois methods devem evitar que você acidentalmente junte duas sequências de caracteres que ambos tenham o delimitador nelas.

  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(); ys.Add(x); ys.AddRange(xs.Skip(2).ToList()); return Combine(ys.ToArray(), delimiter); } 

Razão: Seu segundo URL é considerado um caminho absoluto, o método Combinar retornará somente o último caminho se o último caminho for um caminho absoluto.

Solução: basta remover a barra inicial / do segundo caminho. ( /SecondPath para SecondPath ). então funciona como você excluiu.