【问题标题】:How int + string becomes string?int + string 如何变成字符串?
【发布时间】:2017-06-15 16:30:50
【问题描述】:

我遇到了一种奇怪的方式来实现ToString(),我想知道它是如何工作的:

public string tostr(int n) 
{
    string s = "";
    foreach (char c in n-- + "") {  //<------HOW IS THIS POSSIBLE ?
        s = s + c;
    }
    return s;
}

迭代器是否假定char 的大小?

【问题讨论】:

  • 我希望这不是生产代码...
  • @LmTinyToon 不应该是n.ToString(); n = n - 1; 因为n-- 是一个后减量并将值分配回n
  • 这里后减的意义何在?为什么不只是n + ""
  • 顺便说一句,为什么不只是 public string tostr(int n) { return "" + n; } ???
  • @JasonP 那么除了赏金之外,你到底做了什么?这里已经有很多答案了。

标签: c# .net obfuscation


【解决方案1】:

它隐式调用String.Concat(object, object) 方法,将两个指定对象的字符串表示连接起来:

string result = String.Concat("", n--);

String.Concat(object, object) 方法然后调用String.Concat(string, string)。要阅读Concat 的源并深入检查,请先转到此处:String.cs source code in C# .NET,然后在该页面中搜索TextBox,键入String,然后单击结果中的String.cs 链接以转到C# .NET 中的String.cs 源代码 页面并检查Concat 方法。

这是方法定义:

public static String Concat(Object arg0, Object arg1) 
{ 
    Contract.Ensures(Contract.Result<string>() != null);
    Contract.EndContractBlock(); 

    if (arg0 == null)
    { 
        arg0 = String.Empty;
    }

    if (arg1==null) 
    { 
        arg1 = String.Empty;
    } 
    return Concat(arg0.ToString(), arg1.ToString()); 
}

如你所见,这最终调用了public static String Concat(String str0, String str1) 方法:

public static String Concat(String str0, String str1) 
{
    Contract.Ensures(Contract.Result<string>() != null);
    Contract.Ensures(Contract.Result<string>().Length ==
        (str0 == null ? 0 : str0.Length) + 
        (str1 == null ? 0 : str1.Length));
    Contract.EndContractBlock(); 

    if (IsNullOrEmpty(str0)) {
        if (IsNullOrEmpty(str1)) { 
            return String.Empty;
        }
        return str1;
    } 

    if (IsNullOrEmpty(str1)) { 
        return str0; 
    }

    int str0Length = str0.Length;

    String result = FastAllocateString(str0Length + str1.Length);

    FillStringChecked(result, 0,        str0);
    FillStringChecked(result, str0Length, str1); 

    return result;
}

这是底层的 IL,Ildasm

.method public hidebysig instance string 
        tostr(int32 n) cil managed
{
  // Code size       74 (0x4a)
  .maxstack  3
  .locals init ([0] string s,
           [1] string V_1,
           [2] int32 V_2,
           [3] char c,
           [4] string V_4)
  IL_0000:  nop
  IL_0001:  ldstr      ""
  IL_0006:  stloc.0
  IL_0007:  nop
  IL_0008:  ldarg.1
  IL_0009:  dup
  IL_000a:  ldc.i4.1
  IL_000b:  sub
  IL_000c:  starg.s    n
  IL_000e:  box        [mscorlib]System.Int32
  IL_0013:  call       string [mscorlib]System.String::Concat(object)
  IL_0018:  stloc.1
  IL_0019:  ldc.i4.0
  IL_001a:  stloc.2
  IL_001b:  br.s       IL_0039
  IL_001d:  ldloc.1
  IL_001e:  ldloc.2
  IL_001f:  callvirt   instance char [mscorlib]System.String::get_Chars(int32)
  IL_0024:  stloc.3
  IL_0025:  nop
  IL_0026:  ldloc.0
  IL_0027:  ldloca.s   c
  IL_0029:  call       instance string [mscorlib]System.Char::ToString()
  IL_002e:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0033:  stloc.0
  IL_0034:  nop
  IL_0035:  ldloc.2
  IL_0036:  ldc.i4.1
  IL_0037:  add
  IL_0038:  stloc.2
  IL_0039:  ldloc.2
  IL_003a:  ldloc.1
  IL_003b:  callvirt   instance int32 [mscorlib]System.String::get_Length()
  IL_0040:  blt.s      IL_001d
  IL_0042:  ldloc.0
  IL_0043:  stloc.s    V_4
  IL_0045:  br.s       IL_0047
  IL_0047:  ldloc.s    V_4
  IL_0049:  ret
}// end of method tostr

【讨论】:

  • 我一直认为+ 字符串运算符背后的“魔法”是一个糟糕的设计。运算符并不真正存在,编译器做了一些魔术并调用Concat。我发现1 + s 应该隐式地将数字转换为字符串,这很不直观,这很容易陷入,特别是对于新手程序员。 string.Concat(1, s) 应该是唯一的选择,任何不是 string + string 的都应该是编译时错误。
  • @InBetween 运营商“存在”是什么意思?如果1+1 调用(假设地)Number.Add(1, 1),那会是一个糟糕的设计吗?
  • @immibis 表示string 中没有定义+ 运算符。编译器通过+生成字符串连接的特定代码,它不是类中实现的重载。
  • @InBetween 哦,对了,我忘了 C# 有运算符重载。
  • @InBetween - 也没有为 int32 定义 + 运算符。这是一个“内置”。 String 之所以特别,是因为它首先使用了 C 风格的 Structure Hack 来使其工作。
【解决方案2】:

解释这个“一步一步”:

// assume the input is 1337
public string tostr(int n) {
    //line below is creating a placeholder for the result string
    string s = "";
    // below line we can split into 2 lines to explain in more detail:
    // foreach (char c in n-- + "") {
    // then value of n is concatenated with an empty string :
    // string numberString = n-- + ""; // numberString is "1337";
    // and after this line value of n will be 1336
    // which then is iterated though :
    // foreach(char c in numberString) { // meaning foreach(char c in "1337")
    foreach (char c in n-- + "") {  //<------HOW IS THIS POSSIBLE ?
        s = s + c; // here each sign of the numberString is added into the placeholder
    }
    return s; // return filled placeholder
}

所以基本上如果你将stringint 连接起来,它会自动调用int.ToString 方法并将字符串连接在一起。

【讨论】:

  • 注意n-- 是一个后减量,所以n 被转换为带有n + "" 的字符串,然后被减量。
  • @TheLethalCoder 对不起,我的错误。在更新的答案中修复了这个问题。
【解决方案3】:

n-- 的类型是int,通过使用+ 将其与""(类型为string)连接,将其转换为string。此外,string 实现了IEnumerable&lt;char&gt;,在其上进行了与foreach 的实际迭代。

【讨论】:

    【解决方案4】:

    这段代码看起来难以理解,因为它是我认为该语言中糟糕的设计选择的结果。

    + 运算符在 string 中并不存在。如果您查看参考源或MSDN pagestring 的唯一声明运算符是==!=

    真正发生的是编译器提取了它的一个魔术技巧,并将+ 运算符转换为对静态方法string.Concat 的调用。

    现在,如果您碰巧遇到foreach (char c in string.Concat(n--, "")),您可能会更好地理解代码,因为其意图是明确:我想将两个对象连接为字符串,然后枚举char s 组成了结果字符串。

    当您阅读n-- + "" 时,您的意图远未明确,如果您碰巧拥有n-- + ssstring),情况会更糟。

    两种情况下的编译器都决定您要将参数连接为字符串,并自动将此调用映射到string.Concat(object, object)。 C# 的租户之一是,除非编码员的意图很明确,否则请挥动红旗并要求编码员澄清他的意图。在这种特殊情况下,该租户完全被违反了。

    恕我直言,任何不是string + string 的东西都应该是编译时错误,但那列火车在很多年前就已经过去了。

    【讨论】:

    • 回到过去的美好时光 - 计算机的“黄金时代” - 令人敬畏意味着令人敬畏。
    • 还有一个步骤要避免,即 Concat 的两个参数都调用了它们的 ToString,因此 n-- 将变为 (n--).ToString()
    • 值得注意的是,编译器将字符串上的一长串“+”操作组合成一个 Concat 调用。大概设计团队看到这是开发人员构造字符串的一种非常常见的方式。如果将其实现为标准运算符,则需要单独执行每个 Concat,这有点浪费,因为它会创建一堆中间字符串实例。可以说,由于字符串插值,它不应该再存在了,但它现在会为了向后兼容而存在。
    • @DanBryant 是的,我并不是说它没有用,我只是认为参数应该都是字符串。显式调用ToString 并没有那么麻烦。
    【解决方案5】:

    分解您的代码以显示它发生的原因...

    foreach(char c in n-- + "")
    

    使用+ 运算符将字符串作为操作数之一,只要它具有+ 的实现,无论其他原始操作数是什么,都会将结果转换为字符串。这样做时,n 参与了 string.Concat 方法,正如您从 Autos 调试窗口的以下屏幕截图中看到的那样......

    你可以推断,我用“34”调用了这个方法。那里的循环标识符定义为char c,并且正在通过一个字符串。这是因为string 实现了IEnumerable&lt;char&gt;,正如您从字符串类型的“转到定义”结果的屏幕截图中看到的那样:

    因此,从那时起,它的工作方式与您在任何其他列表/数组中迭代......或更准确地说,IEnumerable 与foreach 并获取每个人char 相同。同时n 已更改为n-- // 33 in my case。此时,n 的值无关紧要,因为您正在遍历表达式 n-- + "" 的结果。也可能是n++,您会得到相同的结果。

    s = s + c; 行应该很容易解释,以防万一,您从n-- + "" 的临时字符串结果中提取的每个字符都附加到您的空(开始时)string s = "";。它产生一个字符串,因为如前所述+ 涉及一个字符串将产生一个字符串。完成所有字符后,它会返回字符串表示形式。

    【讨论】:

    • 关于char 隐式转换为int 的部分在这里完全无关紧要。该代码将int 转换为string,然后遍历字符......在任何时候都没有任何char 被隐式转换为int。其他答案更清楚,IMO。
    • @JonSkeet 感谢您的反馈。我已经删除了关于 char/int 的部分。我认为这会很有帮助,因为我还将谈论字符串是 IEnumerable&lt;char&gt;
    【解决方案6】:

    我使用 ReSharper 将函数转换为 Linq 语句,这可能有助于某些人了解发生了什么(或者只是让人们更加困惑)。

    public string tostrLinq(int n)
    {
        return string.Concat(n--, "").Aggregate("", (string current, char c) => current + c);
    }
    

    正如其他人已经说过的,基本上输入的int 与一个空的string 连接,这基本上为您提供了int 的字符串表示。由于string 实现IEnumberableforeach 循环将字符串分解为char[],在每次迭代中给出字符串的每个字符。然后循环体通过连接每个char 将字符重新组合成一个字符串。

    例如,给定输入5423,它被转换为"5423",然后分解为"5""4""2""3",最后缝合回"5423" .

    现在真正让我头疼的部分是n--。如果这减少了int,那么我们为什么不返回"5422" 呢?直到我阅读了MSDN Article: Increment (++) and Decrement (--) Operators

    增量和减量运算符用作修改的快捷方式 存储在变量中的值并访问该值。任一运算符 可以在前缀或后缀语法中使用。

     If         | Equivalent Action | Return value
    =====================================
    ++variable | variable += 1     | value of variable after incrementing
    variable++ | variable += 1     | value of variable before incrementing
    --variable | variable -= 1     | value of variable after decrementing
    variable-- | variable -= 1     | value of variable before decrementing
    

    所以因为减量运算符在n的末尾应用,所以n的值在n减1之前被string.Concat读取和使用。

    string.Concat(n--,"") 将提供与 string.Contact(n, ""); n = n - 1; 相同的输出。

    所以要获得"5422",我们将其更改为string.Concat(--n, ""),以便n 在传递给string.Contact 之前先递减。

    TL;DR;该功能是一种关于n.ToString()

    的方法

    有趣的是,我还使用 ReSharper 将其转换为 for 循环,但该函数不再起作用,因为 n 在 for 循环的每次迭代中都会递减,这与 foreach 循环不同:

    public string tostrFor(int n)
    {
        string s = "";
        for (int index = 0; index < string.Concat(n--, "").Length; index++)
        {
            char c = string.Concat(n--, "")[index];
            s = s + c;
        }
        return s;
    }
    

    【讨论】:

      【解决方案7】:

      字符串连接:

      …
      string operator +(object x, string y);
      

      二进制+ 运算符在一个或两个时执行字符串连接 操作数是字符串类型。如果字符串连接的操作数是 null,则替换为空字符串。否则,任何非字符串 通过调用 虚拟ToString 方法继承自object 类型。 如果ToString 返回null,则替换为空字符串。 ——ECMA-334,第 201 页。

      所以,n.ToString() 被调用。该方法中的其他所有内容都只是对结果进行分解和重组,没有效果。

      可以写成:

      public string tostr(int n) => n.ToString();
      

      但是,为什么?

      【讨论】:

        【解决方案8】:

        由于n-- 是一个后减量,n 的值仅在连接后才改变。所以本质上它什么都不做。它可能只是

        foreach (char c in n + "")
        

        foreach (char c in (n++) + "")
        

        无论哪种方式都不会改变。原始值被迭代

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-02-14
          • 2011-03-05
          • 2020-10-02
          • 2016-11-09
          • 2021-04-23
          • 1970-01-01
          • 1970-01-01
          • 2017-09-21
          相关资源
          最近更新 更多