CLR でのキャストと 'as' キーワードの使用 質問する

CLR でのキャストと 'as' キーワードの使用 質問する

インターフェイスをプログラミングするときに、キャストやオブジェクト型の変換を頻繁に行っていることに気が付きました。

これら 2 つの変換方法には違いがありますか? もし違いがある場合、コストに違いはありますか、またはプログラムにどのような影響がありますか?

public interface IMyInterface
{
    void AMethod();
}

public class MyClass : IMyInterface
{
    public void AMethod()
    {
       //Do work
    }

    // Other helper methods....
}

public class Implementation
{
    IMyInterface _MyObj;
    MyClass _myCls1;
    MyClass _myCls2;

    public Implementation()
    {
        _MyObj = new MyClass();

        // What is the difference here:
        _myCls1 = (MyClass)_MyObj;
        _myCls2 = (_MyObj as MyClass);
    }
}

また、「一般的に」推奨される方法は何ですか?

ベストアンサー1

下の回答は 2008 年に書かれました。

C# 7 ではパターン マッチングが導入され、演算子はほぼ置き換えられましたas。次のように記述できるようになりました。

if (randomObject is TargetType tt)
{
    // Use tt here
}

ttこの後も はスコープ内にありますが、確実に割り当てられているわけではないことに注意してください。(本体内では確実に割り当てられています ) これは場合によっては少し面倒なので、すべてのスコープで導入する変数の数をできるだけ少なくすることに本当にこだわる場合は、に続いてキャストをif使用することをお勧めします。is


これまでの回答では(この回答を始めた時点では)、どこにどれを使う価値があるのか​​を実際に説明している回答はなかったと思います。

  • 次のようなことはしないでください:

    // Bad code - checks type twice for no reason
    if (randomObject is TargetType)
    {
        TargetType foo = (TargetType) randomObject;
        // Do something with foo
    }
    

    これは 2 回チェックするだけでなく、if がローカル変数ではなくフィールドである場合、異なるものをチェックする可能性があります。別のスレッドが2 つの間randomObjectの値を変更すると、「if」は成功してもキャストが失敗する可能性があります。randomObject

  • randomObjectが本当に のインスタンスであるべきである場合TargetType、つまりそうでない場合はバグがあることを意味し、キャストが正しい解決策です。これにより例外が即座にスローされます。つまり、間違った仮定の下でそれ以上の作業は行われず、例外によってバグの種類が正しく示されます。

    // This will throw an exception if randomObject is non-null and
    // refers to an object of an incompatible type. The cast is
    // the best code if that's the behaviour you want.
    TargetType convertedRandomObject = (TargetType) randomObject;
    
  • が のインスタンスであり、参照型である場合は、次のようなコードを使用しrandomObject ますTargetTypeTargetType

    TargetType convertedRandomObject = randomObject as TargetType;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject
    }
    
  • が のインスタンスでありrandomObject TargetTypeTargetType値型である場合、 自体asは使用できませんTargetTypeが、null 許容型を使用できます。

    TargetType? convertedRandomObject = randomObject as TargetType?;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject.Value
    }
    

    (注:現在、これは実際にはキャストより遅いよりエレガントで一貫性があると思いますが、これで終わりにします。

  • 変換された値が本当に必要なく、それがTargetType のインスタンスであるisかどうかだけを知りたい場合は、演算子が役立ちます。この場合、TargetType が参照型であるか値型であるかは関係ありません。

  • ジェネリックが関係する他のケースでも が役立つ場合がありますis(T が参照型かどうかわからない場合があり、そのため as を使用できないため) が、それらは比較的わかりにくいものです。

  • isこれまでは値型の場合にはほぼ間違いなく使用していましたが、null 許容型とas併用することは考えていませんでした :)


編集: 上記のいずれも、値型の場合を除いてパフォーマンスについては言及していないことに注意してください。値型の場合、null 許容値型へのアンボックス化は実際には遅くなりますが、一貫していることを指摘しました。

naasking さんの回答によると、is-and-cast または is-and-as はどちらも、以下のコードに示すように、最新の JIT を使用した as-and-null-check と同じくらい高速です。

using System;
using System.Diagnostics;
using System.Linq;

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "x";
            values[i + 2] = new object();
        }
        FindLengthWithIsAndCast(values);
        FindLengthWithIsAndAs(values);
        FindLengthWithAsAndNullCheck(values);
    }

    static void FindLengthWithIsAndCast(object[] values)        
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            if (o is string)
            {
                string a = (string) o;
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("Is and Cast: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithIsAndAs(object[] values)        
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            if (o is string)
            {
                string a = o as string;
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("Is and As: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithAsAndNullCheck(object[] values)        
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            string a = o as string;
            if (a != null)
            {
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("As and null check: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }
}

私のラップトップでは、これらはすべて約 60 ミリ秒で実行されます。注意すべき点が 2 つあります。

  • これら 2 つには大きな違いはありません。(実際、as-plus-null-check の方が明らかに遅くなる状況があります上記のコードはシールされたクラス用なので、実際には型チェックが簡単です。インターフェイスをチェックする場合は、as-plus-null-check の方がわずかに有利になります。)
  • これらはすべて非常に高速です。後で値に対して何も行わない場合を除き、これがコードのボトルネックになることはありません。

したがって、パフォーマンスについては心配しないでください。正確性と一貫性について心配しましょう。

変数を扱う場合、is-and-cast (または is-and-as) はどちらも安全ではないと私は考えています。これは、テストとキャストの間にある別のスレッドによって、参照する値の型が変わる可能性があるためです。これはかなりまれな状況ですが、一貫して使用できる規則があるほうが望ましいです。

また、as-then-null-check の方が関心の分離が優れているとも主張します。変換を試みるステートメントが 1 つあり、その結果を使用するステートメントが 1 つあります。is-and-cast または is-and-as はテストを実行し、次に値を変換する別の試みを実行します。

言い換えれば、次のように書く人がいるでしょう:

int value;
if (int.TryParse(text, out value))
{
    value = int.Parse(text);
    // Use value
}

それは、is-and-cast が行っていることと似ていますが、明らかにかなり安価な方法です。

おすすめ記事