【问题标题】:Concatenate ReadOnlySpan<char>连接 ReadOnlySpan<char>
【发布时间】:2018-11-11 01:01:44
【问题描述】:

好的,.NET Core 2.1 已经登陆。有了它,我们获得了一种处理字符串数据ReadOnlySpan&lt;char&gt; 的新方法。它非常适合拆分字符串数据,但是将跨度重新组合在一起呢?

var hello = "Hello".AsSpan();
var space = " ".AsSpan();
var world = "World".AsSpan();

var result = ...; // How do I get "Hello World" out of the 3 above?

【问题讨论】:

  • 跨度通常是指预先存在的内存。为了做你想做的事,你基本上需要分配一个新的字符串或字符数组,然后使用源跨度覆盖它们(是的,你可以覆盖string - 不变性是一个谎言)。 AFAIK 中不容易构建连接。
  • @MarcGravell 有没有关于字符串覆盖的帖子或文章?
  • 如何编写自己的包装器来封装跨度列表并覆盖索引器?
  • @OnurGumus 要获得一致的 API 我需要 resultReadOnlySpan&lt;char&gt;string
  • @hazzik 文档可能很稀疏(我认为它没有记录)...但是例如,您可以查看string.Copy,它使用Buffer.Memcpy 直接修改字符串的内部值通过 wstrcpy。 重要的是你修改的字符串必须是新分配的(例如new String('\0', length)构造函数,或者另一个构造函数,或者.Copy())。参见例如here

标签: c# .net-core .net-core-2.1


【解决方案1】:

以下是 .NET 团队如何在内部为 Path.Join 处理此问题的示例:

private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
{
    Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");

    bool hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
        || PathInternal.IsDirectorySeparator(second[0]);

    fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second))
    {
        return string.Create(
            first.Length + second.Length + (hasSeparator ? 0 : 1),
            (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length, HasSeparator: hasSeparator),
            (destination, state) =>
            {
                new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
                if (!state.HasSeparator)
                    destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
                new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.HasSeparator ? 0 : 1)));
            });
    }
}

如果您想避免使用unsafe 并使用可能更易于阅读的内容,您可以使用类似的内容:

public static ReadOnlySpan<char> Concat(this ReadOnlySpan<char> first, ReadOnlySpan<char> second)
{
    return new string(first.ToArray().Concat(second.ToArray()).ToArray()).AsSpan();
}

public static ReadOnlySpan<char> Concat(this string first, ReadOnlySpan<char> second)
{
    return new string(first.ToArray().Concat(second.ToArray()).ToArray()).ToArray();
}

使用ReadOnlySpan 的级别非常低,并且针对速度进行了优化,因此您如何使用可能取决于您自己的情况。但在许多情况下,返回string 插值和StringBuilder 可能没问题(或者根本不转换为ReadOnlySpan)。所以

var sb = new StringBuilder();
return sb
    .Append(hello)
    .Append(space)
    .Append(world)
    .ToString();

return $"{hello.ToString()}{space.ToString()}{world.ToString()}";

【讨论】:

    【解决方案2】:

    我认为值得一提的是,.NET Core 3 中添加了连接跨度的重载,并且对 .NET Core 2.1 的支持于 2021 年 8 月 21 日结束 [src]。如果您现在升级,那么您可以简单地使用 String.Concat

    https://docs.microsoft.com/en-us/dotnet/api/system.string.concat?view=netcore-3.1#System_String_Concat_System_ReadOnlySpan_System_Char__System_ReadOnlySpan_System_Char__System_ReadOnlySpan_System_Char__

    var hello = "Hello".AsSpan();
    var space = " ".AsSpan();
    var world = "World".AsSpan();
    
    // .NET Core 3+
    var result = string.Concat(hello, space, world);
    

    【讨论】:

      【解决方案3】:

      您可以使用这样的缓冲区来实现这一点 =>

      var hello = "Hello".AsSpan();
      var space = " ".AsSpan();
      var world = "World".AsSpan();
      
      // First allocate the buffer with the target size
      char[] buffer = new char[hello.Length + space.Length + world.Length];
      // "Convert" it to writable Span<char>
      var span = new Span<char>(buffer);
      
      // Then copy each span at the right position in the buffer
      int index = 0;
      hello.CopyTo(span.Slice(index, hello.Length));
      index += hello.Length;
      
      space.CopyTo(span.Slice(index, space.Length));
      index += space.Length;
      
      world.CopyTo(span.Slice(index, world.Length));
      
      // Finality get back the string
      string result = span.ToString();
      

      您可以通过使用 arraypool 来重用缓冲区来再次优化

      char[] buffer =  ArrayPool<char>.Shared.Rent(hello.Length + space.Length + world.Length);
      // ...
      ArrayPool<char>.Shared.Return(buffer);
      

      【讨论】:

      • ArrayPool 没有任何好处。事实上,它更慢(对于这些字符串)并分配了额外的 8 个字节。
      【解决方案4】:

      另一个选项是使用 string.Concat,它接受 ReadOnlySpan 作为参数。这是从github获取的实现

      internal static unsafe string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, ReadOnlySpan<char> str2)
      {
          var result = new string('\0', checked(str0.Length + str1.Length + str2.Length));
          fixed (char* resultPtr = result)
          {
              var resultSpan = new Span<char>(resultPtr, result.Length);
      
              str0.CopyTo(resultSpan);
              resultSpan = resultSpan.Slice(str0.Length);
      
              str1.CopyTo(resultSpan);
              resultSpan = resultSpan.Slice(str1.Length);
      
              str2.CopyTo(resultSpan);
          }
          return result;
      }
      

      https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/Microsoft.IO.Redist/src/Microsoft/IO/StringExtensions.cs

      【讨论】:

      • 请注意,此实现使用 unsafe 关键字,您只能通过在项目的“属性”页面中允许不安全代码(或通过设置 AllowUnsafeBlocks i> 到 true 在您的 csproj 文件中)。对于业务应用程序而言,这通常不是一个好主意。
      【解决方案5】:

      我编写并使用以下扩展/工具方法来连接跨度:

         ReadOnlySpan<T> otherSpan = ...
         T[] someArray = ...
      
         var myArray = someSpan.Concat(otherSpan, someArray, etc);
         var myArray2 = SpanTool.Concat(someArray, otherSpan, etc); 
      

      SpanExtensions.cs

          public static class SpanExtensions {
      
              [MethodImpl(MethodImplOptions.AggressiveInlining)]
              public static T[] Concat<T>(this ReadOnlySpan<T> span0, ReadOnlySpan<T> span1)
                  => SpanTool.Concat(span0, span1);
      
              [MethodImpl(MethodImplOptions.AggressiveInlining)]
              public static T[] Concat<T>(this ReadOnlySpan<T> span0, ReadOnlySpan<T> span1, ReadOnlySpan<T> span2)
                  => SpanTool.Concat(span0, span1, span2);
      
              [MethodImpl(MethodImplOptions.AggressiveInlining)]
              public static T[] Concat<T>(this ReadOnlySpan<T> span0, ReadOnlySpan<T> span1, ReadOnlySpan<T> span2, ReadOnlySpan<T> span3)
                  => SpanTool.Concat(span0, span1, span2, span3);
      
              [MethodImpl(MethodImplOptions.AggressiveInlining)]
              public static T[] Concat<T>(this ReadOnlySpan<T> span0, ReadOnlySpan<T> span1, ReadOnlySpan<T> span2, ReadOnlySpan<T> span3, ReadOnlySpan<T> span4)
                  => SpanTool.Concat(span0, span1, span2, span3, span4);
      
          }
      

      SpanTool.cs

      
       public static class SpanTool {
      
              public static T[] Concat<T>(ReadOnlySpan<T> span0) {
                  var result = new T[span0.Length];
                  span0.CopyTo(result);
                  return result;
              }
      
              public static T[] Concat<T>(ReadOnlySpan<T> span0, ReadOnlySpan<T> span1) {
                  var result = new T[span0.Length + span1.Length];
                  var resultSpan = result.AsSpan();
                  span0.CopyTo(result);
                  var from = span0.Length;
                  span1.CopyTo(resultSpan.Slice(from));
                  return result;
              }
      
              public static T[] Concat<T>(ReadOnlySpan<T> span0, ReadOnlySpan<T> span1, ReadOnlySpan<T> span2) {
                  var result = new T[span0.Length + span1.Length + span2.Length];
                  var resultSpan = result.AsSpan();
                  span0.CopyTo(result);
                  var from = span0.Length;
                  span1.CopyTo(resultSpan.Slice(from));
                  from += span1.Length;
                  span2.CopyTo(resultSpan.Slice(from));
                  return result;
              }
      
              public static T[] Concat<T>(ReadOnlySpan<T> span0, ReadOnlySpan<T> span1, ReadOnlySpan<T> span2, ReadOnlySpan<T> span3) {
                  var result = new T[span0.Length + span1.Length + span2.Length + span3.Length];
                  var resultSpan = result.AsSpan();
                  span0.CopyTo(result);
                  var from = span0.Length;
                  span1.CopyTo(resultSpan.Slice(from));
                  from += span1.Length;
                  span2.CopyTo(resultSpan.Slice(from));
                  from += span2.Length;
                  span3.CopyTo(resultSpan.Slice(from));
                  return result;
              }
      
              public static T[] Concat<T>(ReadOnlySpan<T> span0, ReadOnlySpan<T> span1, ReadOnlySpan<T> span2, ReadOnlySpan<T> span3, ReadOnlySpan<T> span4) {
                  var result = new T[span0.Length + span1.Length + span2.Length + span3.Length + span4.Length];
                  var resultSpan = result.AsSpan();
                  span0.CopyTo(result);
                  var from = span0.Length;
                  span1.CopyTo(resultSpan.Slice(from));
                  from += span1.Length;
                  span2.CopyTo(resultSpan.Slice(from));
                  from += span2.Length;
                  span3.CopyTo(resultSpan.Slice(from));
                  from += span3.Length;
                  span4.CopyTo(resultSpan.Slice(from));
                  return result;
              }
          }
      

      免责声明 在此处的答案不令人满意后,我现在才为我的项目写了这篇文章。如有错误,稍后会编辑。

      【讨论】:

      • 所有这些都为结果分配数组 - 这是意料之中的,因为 [ReadOnly]Span 似乎只用于连续内存。我想知道对于不连续的内存区域是否有类似 Span 的类型。
      【解决方案6】:

      我周五下午的快速思考:

      var hello = "Hello";
      var helloS = hello.AsSpan();
      var spaceS = " ".AsSpan();
      var worldS = "World".AsSpan();
      
      var sentence = helloS.ToString() + spaceS.ToString() + worldS.ToString();
      
      //Gives "Hello World"
      

      至少根据我在 LinqPad 上的快速操作,以及对System.Memory Source 的快速阅读

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-09-13
        • 1970-01-01
        • 2010-12-31
        • 1970-01-01
        • 2018-11-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多