【问题标题】:Why C# using same memory address in this case?为什么在这种情况下 C# 使用相同的内存地址?
【发布时间】:2019-12-27 10:25:14
【问题描述】:

当我执行这段代码时

class Program
    {
        static void Main(string[] args)
        {
//scope 1 
            {
                string x = "shark";
                string y = x.Substring(0);
                unsafe
                {
                    fixed (char* c = y)
                    {
                        c[4] = 'p';
                    }
                }
                Console.WriteLine(x);
            }
//scope 2
            {
                string x = "shark";
//Why output in this line "sharp" and not "shark" ?
                Console.WriteLine(x);
            }
        }
}

输出是:

sharp
sharp

当我在这样的方法中分离这两个范围时:

class Program
    {
        static void Main(string[] args)
        {
            func1();
            func2();
        }
        private static void func2()
        {
            {
                string x = "shark";
                Console.WriteLine(x);
            }
        }

        private static void func1()
        {
            {
                string x = "shark";
                string y = x.Substring(0);
                unsafe
                {
                    fixed (char* c = y)
                    {
                        c[4] = 'p';
                    }
                }
                Console.WriteLine(x);
            }
        }
    }

输出是:

sharp
shark

已编辑

我也这样尝试:

  class Program
    {
        static void Main(string[] args)
        {
            {
                string x = "shark";
                string y = x.Substring(0);
                unsafe
                {
                    fixed (char* c = y)
                    {
                        c[4] = 'p';
                    }
                }
                Console.WriteLine(x);
            }
            void Test(){
                {
                    string x = "shark";
                    Console.WriteLine(x);
                }
            }
            Test();
        }
}

输出是:

 sharp
 shark

**我使用的环境是 MacOS 和 .net core 2.2 (Rider) **

我希望在所有情况下都有相同的输出,但输出不同。正如我们所知,实习是您硬编码的所有字符串都被放入汇编中并在整个应用程序中全局重用,以重用相同的内存空间。但在这段代码的情况下,我们看到

硬编码字符串只能在函数范围内重用,而不是在全局范围内

这是 .NET Core 错误还是有解释?

【问题讨论】:

  • 除此之外,字符串是不可变的,因此您违反了不直接修改字符串内存的长期约定。如果您需要以这种方式修改字符串,请使用 StringBuilder
  • 当您使用unsafe 访问实现定义的详细信息时,为什么会是一个“错误”?你是什​​么字符串类的内部优化,但没有什么是违反任何合同的。
  • “我希望……” 尝试在 C# 或 .NET 规范中找到符合该期望的子句。
  • .NET Core bug 这绝对不是错误。您正在做一些明确标记为unsafe 的事情。然后经历了对您来说意外的行为,因为实习(这是一个实施细节)没有按照您期望的方式行事。未定义的行为是未定义的——规范中没有任何内容说代码不能按照它的行为方式行事。所以绝对不是bug。充其量是奇怪 - 但不是错误。
  • @hovjan:我很想深入研究它,因为我自己还没有看到这种行为,而且我喜欢这种深入的东西。但是,我认为您在这里忽略了要点,那就是您不应该修改字符串。故事结束。

标签: c# .net-core


【解决方案1】:

请注意,自从我写这篇文章后,问题已经改变了

如果您查看source

if( startIndex == 0 && length == this.Length) {
   return this;
}

因此,当您使用 Substring(0) 时,您会获得对原始文件的 reference,然后使用 unsafe

改变

在第二个示例中,Substring(1) 正在分配一个 string


更深入的分析。

string x = "shark";
string y = x.Substring(0);
// x reference and y reference are pointing to the same place

// then you mutate the one memory
c[4] = 'p';

// second scope
string x = "shark";
string y = x.Substring(1);
// x reference and y reference are differnt

// you are mutating y
c[0] = 'p';

编辑

stringinterned,编译器认为"shark" 的任何literal 都是相同的(通过哈希)。这就是为什么第二部分即使使用不同的变量也会产生变异的结果

字符串实习是指拥有每个唯一字符串的单个副本 在一个字符串实习生池中,它是通过 .NET common 中的哈希表 语言运行时 (CLR)。其中键是字符串的哈希值, value 是对实际 String 对象的引用

调试第二部分(没有或没有范围和不同的变量)

编辑 2

范围对我或框架或核心无关紧要,它总是产生相同的结果(第一个),它很可能是一个实现细节,并且在规范中定义模糊的性质

【讨论】:

  • 我了解Substring(0) 内存分配的工作原理,我需要了解为什么第二个作用域中的string x 声明工作错误
  • @hovjan 因为当你执行.Substring(1)时你被分配了一个不同的字符串@
  • @John 当我将新字符串分配给y 时,您能否更详细地解释一下为什么我的第二个x 输出等于第一个x 输出,正如您在我的输出中看到的那样:first x -> 锐利 锐利 秒 x -> 锐利 parp
  • this。 OP 发现,当您加载字符串文字时,它会转到查找表以查找字符串的地址。当您第一次修改字符串时,您修改了查找表中的字符串,因此每个具有相同内容的 literal 都被修改了,因为它在任何地方都使用相同的实例。
  • @madreflection 你是对的。这正是the docs 谈论的行为。
【解决方案2】:

正如已经指出的那样,发生这种情况是因为您正在更改内部字符串本身,这将更改使用该内部字符串的所有内容的字符串。

有趣的是,如果您将这两种方法分开,您确实会看到这种变化:

using System;

namespace CoreApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string x = "shark";
            Console.WriteLine("Main: " + x);

            func2(); // If you comment this out, then the  below call to func2() outputs "shark" instead of "sharp"
            func1();
            Console.WriteLine("Main: " + x);

            func2();
        }

        static void func1()
        {
            string x = "shark";
            string y = x.Substring(0);

            unsafe
            {
                fixed (char* c = y)
                {
                    c[4] = 'p';
                }
            }

            Console.WriteLine("func1(): " + x);
        }

        static void func2()
        {
            string x = "shark";
            Console.WriteLine("func2(): " + x);
        }
    }
}

上面代码的输出是:

Main: shark
func2(): shark
func1(): sharp
Main: sharp
func2(): sharp

有趣的是,如果你注释掉对 func2() 的第一次调用,输出是:

Main: shark
func1(): sharp
Main: sharp
func2(): shark

差异的原因有点难以解释。我认为必须查看生成的实际 IL 以查看是否正在缓存任何内容。

请注意,您可以在不使用的情况下使用不安全的代码更改实习字符串,如下所示:

using System;
using System.Runtime.InteropServices;

namespace CoreApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            const string test  = "ABCDEF"; // Strings are immutable, right?
            char[]       chars = new StringToChar { str = test }.chr;
            chars[0] = 'X';

            // On an x32 release or debug build or on an x64 debug build, 
            // the following prints "XBCDEF".
            // On an x64 release build, it prints "ABXDEF".
            // In both cases, we have changed the contents of 'test' without using
            // any 'unsafe' code...

            Console.WriteLine(test);

            // The following line is even more disturbing, since the constant
            // string "ABCDEF" has been mutated too (because the interned 'constant' string was mutated).

            Console.WriteLine("ABCDEF");
        }
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct StringToChar
    {
        [FieldOffset(0)] public string str;
        [FieldOffset(0)] public char[] chr;
    }
}

当然,这有点令人惊讶,但这不是错误。

【讨论】:

  • 仍然没有回答问题......为什么字符串实习发生在不同的范围内而不是在不同的方法中?我也对此感到困惑,因为从理论上讲,根据文档,它应该发生在应用程序级别。
  • @StefanBalan String interning is 发生在不同的方法中,正如我上面的代码所示(对func1() 的调用会更改func2() 的输出。但是还有一些额外的JIT 编译器引入的复杂性,它可能会生成缓存一些数据并使结果更难解释的代码。
  • 是的,这很奇怪,我开始认为这是实现,因为 op 在 mac 上
  • @hovjan 实际上,我在 Windows 上看到的结果与您得到的结果相同,
  • @hovjan:这是缓存在比 IL 代码更低的级别上,它实际上是查找内部字符串表 docs.microsoft.com/en-us/dotnet/api/… 的 ldstr 操作码 - 当您将其拆分为单独的方法时,它实际上会查找第二次,如果出于某种原因使用相同的方法,它会使用缓存的值。但是您可以检查,如果您将“shark”更改为“sharp”并且 ldstr“sharp”,它将返回指向相同字符串的指针
【解决方案3】:

了解字符串实习https://docs.microsoft.com/en-us/dotnet/api/system.string.intern?view=netframework-4.8

如果您不想有内部引用,则从控制台输入读取字符串或在运行时而不是编译时编写它。此外,字符串应该始终保持不可变,否则可能不仅会导致内部字符串出现问题,还会导致线程安全问题。永远不要触摸设置了.IsInterned 标志的字符串

【讨论】:

  • 你的意思是如果我使用两个作用域 C# using String.Intern ?我不这么认为
  • @hovjan 不,它只是意味着 C# 实习生字符串。
  • @John C# 实习字符串甚至是不同范围的字符串?
  • 范围有什么关系?
  • 你不能在一个范围内定义 2 个同名变量,这就是为什么我添加第二个范围并声明具有相同名称的新变量 x 但 CLR 正在从另一个范围获取值跨度>
【解决方案4】:

逐个函数编译,这就是为什么在第一种情况下,如果您将字符串放在一个范围内,编译器会检查一次字符串并仅放入Intern Pool 一个字符串,并且当您在内存中更改时,它会针对所有硬编码字符串进行更改。

在第二种情况下,当我将字符串放在不同的函数中时,编译器将字符串添加到 Intern Pool 仅用于第一个函数,当我们使用不安全代码更改字符串时,在编译器转到第二种方法并尝试查找字符串后,它会对该字符串应用更改shark 但在Interned Pool 编译器找不到那种字符串(因为我们已经将其更改为sharp),这就是它添加新字符串并且输出变得不同的原因。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-05-31
    • 2014-09-13
    • 2017-01-07
    • 2015-05-20
    • 2013-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多