Приведение против использования ключевого слова as в CLR - PullRequest
367 голосов
/ 30 января 2009

При программировании интерфейсов я обнаружил, что выполняю много типов приведения или преобразования типов.

Есть ли разница между этими двумя методами конвертации? Если да, то есть ли разница в стоимости или как это повлияет на мою программу?

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);
    }
}

Кроме того, что является "вообще" предпочтительным методом?

Ответы [ 18 ]

494 голосов
/ 30 января 2009

Ответ под строкой был написан в 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
    }
    

    Эта проверка не только повторяется дважды, но и может проверять разные вещи, если randomObject является полем, а не локальной переменной. Возможно, что «если» пройдет, но тогда приведение не будет выполнено, если другой поток изменит значение 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 может быть экземпляром TargetType и TargetType является ссылочным типом, используйте код, подобный следующему:

    TargetType convertedRandomObject = randomObject as TargetType;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject
    }
    
  • Если randomObject может быть экземпляром TargetType и TargetType является типом значения, то мы не можем использовать as с самой TargetType, но мы можно использовать обнуляемый тип:

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

    (Примечание: в настоящее время это на самом деле медленнее, чем + + . Я думаю, что это более элегантно и последовательно, но мы идем.)

  • Если вам действительно не нужно преобразованное значение, но вам просто нужно знать, является ли оно экземпляром TargetType, тогда оператор is - ваш друг. В этом случае не имеет значения, является ли TargetType ссылочным типом или типом значения.

  • В других случаях, связанных с генериками, могут быть полезны is (потому что вы можете не знать, является ли T ссылочным типом или нет, поэтому вы не можете использовать как), но они относительно неясны.

  • Я почти наверняка использовал is для случая типа значения до сих пор, не задумываясь об использовании обнуляемого типа и as вместе:)


РЕДАКТИРОВАТЬ: обратите внимание, что ни один из вышеперечисленных не говорит о производительности, кроме случая типа значения, где я отметил, что распаковка в тип значения, допускающий значение NULL, на самом деле медленнее - но согласованно.

В соответствии с ответом Нааскинга, is-and-cast или is-and-as являются такими же быстрыми, как и нулевая проверка с современными JIT, как показано кодом ниже:

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 мс. Следует отметить две вещи:

  • Между ними нет существенной разницы. (На самом деле, бывают ситуации, когда проверка as-plus-null определенно медленнее . Приведенный выше код на самом деле облегчает проверку типа, потому что это для запечатанного класса; если вы проверяете для интерфейс, баланс немного склоняется в пользу проверки как плюс-ноль.)
  • Они все безумно быстро. Это просто не будет узким местом в вашем коде, если вы действительно не собираетесь делать что-нибудь со значениями впоследствии.

Так что давайте не будем беспокоиться о производительности. Давайте позаботимся о правильности и последовательности.

Я утверждаю, что is-and-cast (или is-and-as) оба небезопасны при работе с переменными, поскольку тип значения, на которое он ссылается, может измениться из-за другого потока между тестом и приведением. Это было бы довольно редкой ситуацией - но я бы предпочел соглашение, которое я мог бы использовать последовательно.

Я также утверждаю, что проверка "как тогда ноль" дает лучшее разделение проблем. У нас есть одно утверждение, которое пытается преобразовать, а затем одно утверждение, которое использует результат. Метод is-and-cast или is-and-as выполняет проверку, а затем еще одну попытку преобразовать значение.

Другими словами, может ли кто-нибудь когда-либо написать:

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

Это то, что делает актерский состав и актерский состав - хотя, очевидно, более дешевым способом.

67 голосов
/ 30 января 2009

"as" вернет NULL, если невозможно разыграть.

приведение перед вызовет исключение.

Для производительности повышение исключения обычно обходится дороже.

24 голосов
/ 30 января 2009

Вот еще один ответ, с некоторым сравнением IL. Рассмотрим класс:

public class MyClass
{
    public static void Main()
    {
        // Call the 2 methods
    }

    public void DirectCast(Object obj)
    {
        if ( obj is MyClass)
        { 
            MyClass myclass = (MyClass) obj; 
            Console.WriteLine(obj);
        } 
    } 


    public void UsesAs(object obj) 
    { 
        MyClass myclass = obj as MyClass; 
        if (myclass != null) 
        { 
            Console.WriteLine(obj);
        } 
    }
}

Теперь посмотрите на IL, который производит каждый метод. Даже если операционные коды ничего не значат для вас, вы можете увидеть одно существенное отличие - вызывается isinst, за которым следует castclass в методе DirectCast. Так что два звонка вместо одного в принципе.

.method public hidebysig instance void  DirectCast(object obj) cil managed
{
  // Code size       22 (0x16)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  isinst     MyClass
  IL_0006:  brfalse.s  IL_0015
  IL_0008:  ldarg.1
  IL_0009:  castclass  MyClass
  IL_000e:  pop
  IL_000f:  ldarg.1
  IL_0010:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0015:  ret
} // end of method MyClass::DirectCast

.method public hidebysig instance void  UsesAs(object obj) cil managed
{
  // Code size       17 (0x11)
  .maxstack  1
  .locals init (class MyClass V_0)
  IL_0000:  ldarg.1
  IL_0001:  isinst     MyClass
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  brfalse.s  IL_0010
  IL_000a:  ldarg.1
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ret
} // end of method MyClass::UsesAs

Ключевое слово isinst по сравнению с кастом

В этом блоге есть достойное сравнение между двумя способами сделать это. Его резюме:

  • В прямом сравнении isinst быстрее чем Castclass (хотя и незначительно)
  • Когда необходимо выполнить проверку, чтобы убедиться, что преобразование прошло успешно, isinst был значительно быстрее, чем castclass
  • Не следует использовать комбинацию isinst и castclass, так как это было намного медленнее, чем самое быстрое "безопасное" преобразование (более чем на 12% медленнее)

Лично я всегда использую As, потому что он легко читается и рекомендуется командой разработчиков .NET (или Джеффри Рихтером в любом случае)

18 голосов
/ 30 января 2009

Одно из наиболее тонких различий между ними заключается в том, что ключевое слово "as" нельзя использовать для приведения, когда задействован оператор приведения:

public class Foo
{
    public string Value;

    public static explicit operator string(Foo f)
    {
        return f.Value;
    }

}

public class Example
{
    public void Convert()
    {
        var f = new Foo();
        f.Value = "abc";

        string cast = (string)f;
        string tryCast = f as string;
    }
}

Это не скомпилирует (хотя я думаю, что это было в предыдущих версиях) в последней строке, так как ключевые слова "как" не учитывают операторы приведения. Линия string cast = (string)f; работает просто отлично.

12 голосов
/ 30 января 2009

как никогда не генерирует исключение, если не может выполнить преобразование, возвращая ноль (, поскольку работает только с ссылочными типами). Таким образом, использование в качестве в основном эквивалентно

_myCls2 = _myObj is MyClass ? (MyClass)_myObj : null;

С другой стороны, приведения в стиле C выдает исключение, если преобразование невозможно.

10 голосов
/ 30 января 2009

Не совсем ответ на ваш вопрос, но я думаю, что это важный связанный вопрос.

Если вы программируете для интерфейса, вам не нужно выполнять приведение. Надеюсь, эти броски очень редки. Если нет, то вам, вероятно, нужно переосмыслить некоторые из ваших интерфейсов.

9 голосов
/ 14 ноября 2009

Пожалуйста, не обращайте внимания на советы Джона Скита, избегайте шаблонов «тест-и-бросок», т.е.

if (randomObject is TargetType)
{
    TargetType foo = randomObject as TargetType;
    // Do something with foo
}

Идея, что это стоит больше, чем бросок и нулевой тест, МИФ :

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

Это микрооптимизация, которая не работает. Я выполнил некоторые реальные тесты , и тестирование-и-приведение на самом деле быстрее, чем сравнение-и-ноль-сравнение, и это также более безопасно, потому что у вас нет возможности иметь нулевую ссылку в области видимости вне if, если произойдет сбой приведения.

Если вам нужна причина, по которой тестирование и приведение быстрее или, по крайней мере, не медленнее, есть простая и сложная причина.

Простой: даже наивные компиляторы объединят две одинаковые операции, такие как test-and-cast, в один тест и ветвь. cast-and-null-test может вызвать два теста и ветвь, один для проверки типа и преобразования в null при ошибке, один для самой проверки null. По крайней мере, они оба будут оптимизированы для одного теста и ветвления, поэтому тестирование и приведение не будут ни медленнее, ни быстрее, чем приведение и приведение к нулю.

Сложный: почему тестирование и приведение происходит быстрее: приведение и преобразование в ноль вводят во внешнюю область видимости другую переменную, которую компилятор должен отслеживать для обеспечения жизнеспособности, и он может не иметь возможности оптимизировать эта переменная в зависимости от того, насколько сложен ваш поток управления. И наоборот, тестирование и приведение вводят новую переменную только в области с разделителями, поэтому компилятор знает, что переменная устарела после выхода из области, и поэтому может лучше оптимизировать распределение регистров.

Так что, пожалуйста, ПОЖАЛУЙСТА, пусть этот «бросок-и-ноль-проверка лучше, чем проверка-и-бросок» совет DIE. ПОЖАЛУЙСТА. Тестирование и приведение более безопасны и быстрее.

4 голосов
/ 30 января 2009

Если приведение не выполнено, ключевое слово «as» не вызывает исключение; вместо этого он устанавливает для переменной значение null (или значение по умолчанию для типов значений).

4 голосов
/ 30 января 2009

Это не ответ на вопрос, а комментарий к примеру кода вопроса:

Обычно вам не нужно разыгрывать объект, например, из IMyInterface для MyClass. Отличительной особенностью интерфейсов является то, что если вы берете объект в качестве входных данных, который реализует интерфейс, то вам не нужно заботиться о том, какой объект вы получаете.

Если вы приведете IMyInterface к MyClass, вы уже предполагаете, что получили объект типа MyClass, и нет смысла использовать IMyInterface, потому что если вы передадите свой код другим классам, которые реализуют IMyInterface, это нарушит ваш код. ..

Теперь мой совет: если ваши интерфейсы хорошо спроектированы, вы можете избежать большого количества типов.

3 голосов
/ 30 января 2009

Оператор as может использоваться только для ссылочных типов, он не может быть перегружен и вернет null в случае сбоя операции. Это никогда не вызовет исключения.

Приведение может использоваться на любых совместимых типах, оно может быть перегружено и выдает исключение в случае сбоя операции.

Выбор того, что использовать, зависит от обстоятельств. Прежде всего, это вопрос того, хотите ли вы выдать исключение при неудачном преобразовании.

...