C#文字列参照型?


163

C#の "string"は参照型であることを知っています。これはMSDNにあります。ただし、このコードは次のように機能しません。

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(string test)
    {
        test = "after passing";
    }
}

文字列をパラメーターとして渡し、それが参照型であるため、出力は「受け渡し前」「受け渡し後」になるはずです。2番目の出力ステートメントは、TestIメソッドでテキストが変更されたことを認識します。ただし、「渡される前」「渡される前」が表示されるため、参照ではなく値で渡されているように見えます。文字列は不変であることは理解していますが、ここで何が行われているのかがわかりません。何が欠けていますか?ありがとう。


以下のJonが参照している記事を参照してください。言及した動作は、C ++ポインターでも再現できます。
Sesh

MSDNでも非常に良い説明。
Dimi_Pel

回答:


211

文字列への参照は値で渡されます。値による参照の受け渡しと参照によるオブジェクトの受け渡しには大きな違いがあります。「リファレンス」という言葉が両方のケースで使用されているのは残念です。

あなたがいる場合行う文字列参照を渡すことで参照あなたが期待するように、それは動作します:

using System;

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(ref test);
        Console.WriteLine(test);
    }

    public static void TestI(ref string test)
    {
        test = "after passing";
    }
}

次に、参照が参照するオブジェクトに変更を加えることと、変数(パラメーターなど)に変更を加えて、別のオブジェクトを参照させるようにすることを区別する必要があります。文字列は不変であるため、文字列を変更することはできませんが、StringBuilder代わりにaを使用してそれを示すことができます。

using System;
using System.Text;

class Test
{
    public static void Main()
    {
        StringBuilder test = new StringBuilder();
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(StringBuilder test)
    {
        // Note that we're not changing the value
        // of the "test" parameter - we're changing
        // the data in the object it's referring to
        test.Append("changing");
    }
}

詳細については、パラメーターの受け渡しに関する私の記事を参照しください。


2
同意します。ref修飾子の使用は非参照型でも機能することを明確にしたいだけです。つまり、どちらもかなり別々の概念です。
eglasius 2009

2
@Jon Skeetがあなたの記事の傍注を追加しました。あなたはreferencedそれをあなたの答えとして持っているべきです
Nithish Inpursuit Ofhappiness

36

質問に答える必要がある場合:文字列は参照型であり、参照として動作します。実際の文字列ではなく、参照を保持するパラメーターを渡します。問題は関数にあります:

public static void TestI(string test)
{
    test = "after passing";
}

パラメータtestは文字列への参照を保持しますが、それはコピーです。文字列を指す2つの変数があります。また、文字列を使用した操作では実際に新しいオブジェクトが作成されるため、新しい文字列を指すようにローカルコピーを作成します。ただし、元のtest変数は変更されません。

変数refの値testは渡さず、参照のみを渡すため、関数宣言と呼び出し作業に含める推奨ソリューション。したがって、関数内の変更はすべて元の変数を反映します。

最後に繰り返したいと思います。文字列は参照型ですが、不変なので、行はtest = "after passing";実際に新しいオブジェクトを作成し、変数のコピーはtest新しい文字列を指すように変更されます。


25

他の人が述べたように、String.NET の型は不変であり、その参照は値で渡されます。

元のコードでは、この行が実行されるとすぐに:

test = "after passing";

その後、元のオブジェクトを参照testなくなります。新しいオブジェクトを作成し、マネージヒープ上のそのオブジェクトを参照するように割り当てました。 Stringtest

思い出させるための目に見える正式なコンストラクタがないため、多くの人がここにつまずくように感じます。この場合、String型はその構築方法で言語をサポートしているため、裏で発生しています。

したがって、これがへの変更がメソッドtestのスコープの外に表示されない理由ですTestI(string)-値によって参照を渡し、その値が変更されました!しかし、String参照が参照によって渡された場合、参照が変更されると、TestI(string)メソッドのスコープ外で参照が表示されます。

この場合、refまたはoutキーワードのいずれかが必要です。outこの特定の状況には、キーワードの方が少し適していると思います。

class Program
{
    static void Main(string[] args)
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(out test);
        Console.WriteLine(test);
        Console.ReadLine();
    }

    public static void TestI(out string test)
    {
        test = "after passing";
    }
}

ref =関数の外部で初期化、out =関数の内部で初期化、言い換えれば refは双方向で、outはout-onlyです。したがって、確かにrefを使用する必要があります。
Paul Zahra

@PaulZahra:outコードをコンパイルするには、メソッド内で割り当てる必要があります。refそのような要件はありません。また、outパラメーターはメソッドの外部で初期化されます-この回答のコードは反例です。
デレクW

明確にする必要があります- outパラメータはメソッドの外初期化できますが、そうする必要はありません。この場合は、outパラメーターを初期化してstring、.NET の型の性質に関するポイントを示します。
Derek W

9

実際、どのオブジェクトでも同じでした。つまり、参照型であることと、参照渡しは、c#では2つの異なることです。

これは機能しますが、タイプに関係なく適用されます。

public static void TestI(ref string test)

文字列が参照型であることについても、それは特別なものです。不変であるように設計されているため、すべてのメソッドはインスタンスを変更しません(新しいインスタンスを返します)。また、パフォーマンスのためにいくつかの追加の要素があります。


7

値タイプ、値渡し、参照タイプ、および参照渡しの違いについて考える良い方法を次に示します。

変数はコンテナです。

値型変数にはインスタンスが含まれます。参照型の変数には、他の場所に格納されているインスタンスへのポインタが含まれています。

値型変数を変更すると、その変数に含まれるインスタンスが変化します。参照型変数を変更すると、それが指すインスタンスが変化します。

個別の参照型変数は、同じインスタンスを指すことができます。したがって、同じインスタンスは、それを指す変数を介して変更できます。

値渡しの引数は、コンテンツの新しいコピーを含む新しいコンテナです。参照渡しの引数は、元のコンテンツを持つ元のコンテナです。

値タイプの引数が値で渡される場合:コンテナは一意であるため、引数のコンテンツを再割り当てしてもスコープ外では効果がありません。インスタンスは独立したコピーであるため、引数を変更してもスコープ外では効果がありません。

参照タイプの引数が値渡しの場合:コンテナは一意であるため、引数のコンテンツを再割り当てしてもスコープ外では効果がありません。引数の内容を変更すると、コピーされたポインターが共有インスタンスを指すため、外部スコープに影響します。

引数が参照渡しされる場合:コンテナーが共有されているため、引数のコンテンツを再割り当てすると、外部スコープに影響します。引数のコンテンツを変更すると、コンテンツが共有されるため、外部スコープに影響します。

結論として:

文字列変数は参照型の変数です。したがって、他の場所に格納されているインスタンスへのポインタが含まれています。値で渡されると、そのポインタがコピーされるため、文字列引数を変更すると、共有インスタンスに影響を与えるはずです。ただし、文字列インスタンスには変更可能なプロパティがないため、文字列引数を変更することはできません。参照渡しの場合、ポインターのコンテナーは共有されるため、再割り当ては引き続き外部スコープに影響します。


6

絵は千の言葉に値する」。

ここに簡単な例があります。それはあなたのケースに似ています。

string s1 = "abc";
string s2 = s1;
s1 = "def";
Console.WriteLine(s2);
// Output: abc

これが起こったことです:

ここに画像の説明を入力してください

  • 1行目と2:s1s2同じに、変数の参照"abc"文字列オブジェクト。
  • 3行目: 文字列は不変であるため、"abc"文字列オブジェクトはそれ自体を(に"def")変更しませんが、"def"代わりに新しい文字列オブジェクトが作成され、それをs1参照します。
  • 4行目:s2まだ参照しています"abc"文字列オブジェクトをため、これが出力です。

5

上記の回答は役に立ちます。パラメーターが参照型であっても、refキーワードなしでパラメーターを渡すとどうなるかを明確に示す例を追加したいと思います。

MyClass c = new MyClass(); c.MyProperty = "foo";

CNull(c); // only a copy of the reference is sent 
Console.WriteLine(c.MyProperty); // still foo, we only made the copy null
CPropertyChange(c); 
Console.WriteLine(c.MyProperty); // bar


private void CNull(MyClass c2)
        {          
            c2 = null;
        }
private void CPropertyChange(MyClass c2) 
        {
            c2.MyProperty = "bar"; // c2 is a copy, but it refers to the same object that c does (on heap) and modified property would appear on c.MyProperty as well.
        }

1
この説明は私にとって最も効果的でした。したがって、refキーワード(またはout)を使用しない限り、変数自体が値または参照型であるという事実にもかかわらず、基本的にすべてを値で渡します。通常、オブジェクトをnullに設定したり、オブジェクトが渡されたメソッド内の別のインスタンスに設定したりするのではなく、プロパティを設定したり、メソッドを呼び出したりするため、日常のコーディングでは目立ちません。"string"の場合、新しいインスタンスに設定することは常に発生しますが、更新は表示されず、訓練されていない目に誤った解釈を与えます。間違っていれば私を訂正してください。
ΕГИІИО

3

好奇心旺盛な方のために、そして会話を完了するために: はい、文字列は参照型です:

unsafe
{
     string a = "Test";
     string b = a;
     fixed (char* p = a)
     {
          p[0] = 'B';
     }
     Console.WriteLine(a); // output: "Best"
     Console.WriteLine(b); // output: "Best"
}

ただし、この変更は安全でないブロックでのみ機能することに注意してください。文字列は不変であるため (MSDNから):

文字列オブジェクトの内容は、オブジェクトが作成された後は変更できませんが、構文により、これを実行できるかのように表示されます。たとえば、このコードを作成すると、コンパイラは実際に新しい文字列オブジェクトを作成して新しい文字シーケンスを保持し、その新しいオブジェクトがbに割り当てられます。文字列 "h"はガベージコレクションの対象になります。

string b = "h";  
b += "ello";  

次の点に注意してください。

文字列は参照型ですが、等価演算子(==および !=)は、参照ではなく文字列オブジェクトの値を比較するために定義されています。


0

あなたのコードは次のようなものだと思います。ここにないのと同じ理由で、値が変更されることを期待するべきではありませんでした。

 public static void Main()
 {
     StringWrapper testVariable = new StringWrapper("before passing");
     Console.WriteLine(testVariable);
     TestI(testVariable);
     Console.WriteLine(testVariable);
 }

 public static void TestI(StringWrapper testParameter)
 {
     testParameter = new StringWrapper("after passing");

     // this will change the object that testParameter is pointing/referring
     // to but it doesn't change testVariable unless you use a reference
     // parameter as indicated in other answers
 }

-1

試してください:


public static void TestI(ref string test)
    {
        test = "after passing";
    }

3
答えには、コードだけではありません。また、それが機能する理由についての説明も含める必要があります。
Charles Caldwell
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.