Имеет ли смысл использовать «как» вместо приведения, даже если нет нулевой проверки? [закрыто]

351

В блогах по разработке, онлайн-примерах кода и (в последнее время) даже в книге я постоянно спотыкаюсь о коде:

var y = x as T;
y.SomeMethod();

или, что еще хуже:

(x as T).SomeMethod();

Это не имеет смысла для меня. Если вы уверены , что xэто типа T, вы должны использовать прямой бросок: (T)x. Если вы не уверены, вы можете использовать, asно нужно проверить nullперед выполнением какой-либо операции. Все, что делает приведенный выше код, это превращает (полезное) InvalidCastExceptionв (бесполезное) NullReferenceException.

Я единственный, кто считает, что это вопиющее злоупотребление asключевым словом? Или я упустил что-то очевидное, и приведенная выше схема действительно имеет смысл?

Heinzi
источник
56
Было бы смешнее видеть (целовать как S) .SteveIsSuchA (); Но я согласен, это злоупотребление.
SwDevMan81
5
Это гораздо круче, чем писать ((T)x).SomeMethod(), не так ли? ;) (шучу, ты прав конечно!)
Лусеро
8
@P Папа Я не согласен, совершенно хороший вопрос (действительно ли этот кодовый код имеет смысл) и очень полезный. +1 к вопросу и недовольство тем, кто голосует за закрытие.
MarkJ
9
Люцерно прав, этот паттерн кодирования вызывается попыткой избежать скобок. Неизлечим после контакта с Лиспом.
Ганс Пассант
13
Оптимизированный код (f as T).SomeMethod()
:;

Ответы:

252

Ваше понимание верно. Это звучит как попытка микрооптимизации для меня. Вы должны использовать обычное приведение, когда вы уверены в типе. Помимо генерации более разумного исключения, оно также быстро дает сбой. Если вы ошибаетесь в своем предположении о типе, ваша программа немедленно завершится сбоем, и вы сможете сразу увидеть причину сбоя, а не ждать NullReferenceExceptionили ArgumentNullExceptionдаже логической ошибки в будущем. Как правило, asвыражение, за которым не следует nullпроверка, является запахом кода.

С другой стороны, если вы не уверены в касте и ожидаете, что он потерпит неудачу, вы должны использовать asвместо обычного броска, обернутого try-catchблоком. Кроме того, asрекомендуется использование над проверкой типа, сопровождаемой приведением. Вместо:

if (x is SomeType)
   ((SomeType)x).SomeMethod();

который генерирует isinstинструкцию для isключевого слова и castclassинструкцию для приведения (эффективно выполняя приведение дважды), вы должны использовать:

var v = x as SomeType;
if (v != null)
    v.SomeMethod();

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


Следующее решение не рекомендуется для использования в производственном коде. Если вы действительно ненавидите такую ​​фундаментальную конструкцию в C #, вы можете подумать о переходе на VB или другой язык.

В случае, если кто-то отчаянно ненавидит синтаксис преобразования, он / она может написать метод расширения для имитации преобразования:

public static T To<T>(this object o) { // Name it as you like: As, Cast, To, ...
    return (T)o;
}

и используйте аккуратный синтаксис [?]:

obj.To<SomeType>().SomeMethod()
Мехрдад Афшари
источник
5
Я думаю, что состояние гонки не имеет значения. Если вы столкнулись с этой проблемой, то ваш код не является поточно-ориентированным, и есть более надежные способы его решения, чем использование ключевого слова «как». +1 для остальной части ответа.
RMorrisey
10
@RMorrisey: Я имею в виду, по крайней мере, один пример: предположим, у вас есть cacheобъект, который другой поток пытается сделать недействительным, установив его null. В сценариях без блокировки такие вещи могут возникать.
Мехрдад Афшари
10
is + cast достаточно, чтобы вызвать предупреждение FxCop «Не приводить без необходимости»: msdn.microsoft.com/en-us/library/ms182271.aspx Этого должно быть достаточно, чтобы избежать конструкции.
Дэвид Шмитт
2
Вы должны избегать использования методов расширения Object. Использование метода для типа значения приведет к тому, что он будет упакован без необходимости.
MgSam
2
@MgSam Очевидно, что такой вариант использования не имеет смысла для Toметода здесь, поскольку он конвертирует только через иерархию наследования, которая для типов значений в любом случае включает в себя упаковку. Конечно, вся идея скорее теоретическая, чем серьезная.
Мехрдад Афшари
42

ИМХО, asпросто имеет смысл в сочетании с nullпроверкой

var y = x as T;
if (y != null)
    y.SomeMethod();
Рубенс Фариас
источник
40

Использование «как» не применяет определенные пользователем преобразования, в то время как приведение будет использовать их там, где это необходимо. Это может быть важным отличием в некоторых случаях.

Ларри Фикс
источник
5
Это важно помнить. Эрик Липперт рассказывает об
P Daddy
5
Хороший комментарий, P! Если ваш код зависит от этого различия, я бы сказал, что в будущем у вас будет поздняя отладочная сессия.
TrueWill
36

Я написал немного об этом здесь:

http://blogs.msdn.com/ericlippert/archive/2009/10/08/what-s-the-difference-between-as-and-cast-operators.aspx

Я понимаю вашу точку зрения. И я согласен с этим: оператор приведения сообщает: «Я уверен, что этот объект может быть преобразован в этот тип, и я готов рискнуть исключением, если ошибаюсь», тогда как оператор «как» сообщает «Я не уверен, что этот объект можно преобразовать в этот тип; если я ошибаюсь, дайте мне ноль».

Тем не менее, есть небольшая разница. (x как T). Что бы ни () сообщало: «Я знаю не только то, что x можно преобразовать в T, но, кроме того, это включает в себя только ссылочные или распаковывающие преобразования, и, кроме того, что x не является нулевым». Это действительно передает информацию, отличную от ((T) x) .Wh независимо (), и, возможно, именно это намеревается автор кода.

Эрик Липперт
источник
1
Я не согласен с вашей умозрительной защитой автора кода в вашем последнем предложении. ((T)x).Whatever() также сообщает, что xэто [не должно быть] пустым, и я очень сомневаюсь, что автору, как правило, будет интересно, Tпроисходит ли преобразование только с ссылочными или распакованными преобразованиями, или требуется ли преобразование, определяемое пользователем или изменяющее представление. В конце концов, если я определюсь public static explicit operator Foo(Bar b){}, то, несомненно, мое намерение Barбудет считаться совместимым с Foo. Я редко хотел бы избежать этого преобразования.
P Daddy
4
Что ж, возможно, большинство авторов кода не будут делать этого тонкого различия. Лично я мог бы быть, но если бы я был, я бы добавил комментарий на этот счет.
Эрик Липперт
16

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

Одним из наиболее очевидных вводящих в заблуждение аспектов этой статьи является график, который не указывает, что измеряется: я подозреваю, что он измеряет неудачные приведения (где «как», очевидно, намного быстрее, поскольку не выбрасывается исключение).

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

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

Джо
источник
2
Спасибо за ссылку, это очень интересно. От того, как я понял статью, он делает сравнение случая без исключения. Тем не менее, статья была написана для .net 1.1, и в комментариях отмечается, что это изменилось в .net 2.0: производительность теперь почти одинакова, при этом префикс преобразуется даже немного быстрее.
Хайнци
1
В статье подразумевается, что он сравнивает случай, не являющийся исключением, но я давным-давно провел некоторые тесты и не смог воспроизвести его заявленные результаты, даже с .NET 1.x. А поскольку статья не содержит кода, используемого для запуска теста, невозможно сказать, что сравнивается.
Джо
«Грузовой культ» - отлично. Проверьте "Cargo Cult Science Ричард Фейнман" для полной информации.
Боб Денни
11

Прямое приведение требует пары скобок больше, чем asключевое слово. Так что даже в том случае, если вы на 100% уверены, что это за тип, это уменьшает визуальный беспорядок.

Договорились об исключении, хотя. Но, по крайней мере, для меня, большинство применений asсводятся к тому, чтобы nullпотом проверить , что для меня лучше, чем поймать исключение.

детеныш
источник
8

99% времени, когда я использую «как», это когда я не уверен, какой тип объекта на самом деле

var x = obj as T;
if(x != null){
 //x was type T!
}

и я не хочу ни отлавливать явные исключения приведения, ни делать приведения дважды, используя «is»:

//I don't like this
if(obj is T){
  var x = (T)obj; 
}
Макс Галкин
источник
8
Вы только что описали правильный вариант использования as. Какой еще 1%?
P Daddy
В опечатке? =) Я имел в виду 99% времени, когда я использую этот точный фрагмент кода, хотя иногда я могу использовать «как» в вызове метода или в каком-то другом месте.
Макс Галкин
О, а как это менее полезно, чем второй популярный ответ ???
Макс Галкин
2
+1 Я согласен, что это так же ценно, как и ответ Рубенса Фариаса - надеюсь, что люди придут сюда, и это будет полезным примером
Рубен Бартелинк
8

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

Посмотрим правде в глаза: оператор приведения / преобразования в C-подобных языках довольно ужасен с точки зрения читаемости. Я хотел бы лучше, если бы C # принял любой синтаксис Javascript:

object o = 1;
int i = int(o);

Или определите toоператор, эквивалент преобразования as:

object o = 1;
int i = o to int;
JulianR
источник
Как вы знаете, синтаксис JavaScript, который вы упоминаете, также разрешен в C ++.
P Daddy
@PDaddy: Это не 100% совместимый альтернативный синтаксис, хотя и не предназначенный для этого (оператор X против конструктора преобразования)
Рубен Бартелинк
Я бы предпочел использовать синтаксис C ++ dynamic_cast<>()(и тому подобное). Вы делаете что-то уродливое, это должно выглядеть уродливо.
Том Хотин - tackline
5

Людям asтак нравится, потому что это заставляет их чувствовать себя в безопасности от исключений ... Как гарантия на коробке. Парень ставит на коробку причудливую гарантию, потому что хочет, чтобы внутри вас было тепло и жарко. Вы полагаете, что положили эту маленькую коробочку под подушку ночью, Фея Гарантии может спуститься и уйти через четверть, я прав, Тед?

Вернемся к теме ... при использовании прямого приведения существует вероятность недопустимого исключения приведения. Таким образом, люди применяют asв качестве общего решения для всех своих потребностей в кастинге, потому что as(само по себе) никогда не вызовут исключения. Но забавно то, что в приведенном вами примере (x as T).SomeMethod();вы заменяете недопустимое исключение приведения на исключение нулевой ссылки. Что скрывает реальную проблему, когда вы видите исключение.

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

боб
источник
2
«Я предпочитаю is is» - «is», за которым следует приведение, конечно, медленнее, чем «as», за которым следует проверка на нулевое значение (точно так же, как «IDictionary.ContainsKey», за которым следует разыменование с использованием индексатора медленнее, чем «IDictionary.TryGetValue» «). Но если вы найдете его более читабельным, без сомнения, разница редко бывает значительной.
Джо
Важное утверждение в средней части - это то, как люди применяют asв качестве общего решения, потому что оно заставляет их чувствовать себя в безопасности.
Боб
5

Это должно быть одним из моих главных раздражителей .

D & E Страуструпа и / или какой-то пост в блоге, который я не могу найти прямо сейчас, обсуждает понятие toоператора, который бы обращал внимание на точку зрения https://stackoverflow.com/users/73070/johannes-rossel (т. Е. Тот же синтаксис, что и asс DirectCastсемантикой ).

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

Жаль, что «умные» программисты (часто авторы книг (Ювал Лоуи IIRC)) обходят это, злоупотребляя asэтим способом (C ++ не предлагает as, вероятно, по этой причине).

Даже VB обладает большей последовательностью, имея единый синтаксис, который заставляет вас выбирать « TryCastили» DirectCastи принимать решение !

Рубен Бартелинк
источник
+1. Вы, вероятно, имели в виду DirectCast поведение , а не синтаксис .
Хайнци
@Heinzi: Та за +1. Хорошая точка зрения. Решил быть умным и использовать semanticsвместо этого: P
Рубен Бартелинк
Учитывая, что C # не претендует на совместимость с C, C ++ или Java, я нахожусь в ярости от некоторых вещей, которые он заимствует из этих языков. Это выходит за рамки «я знаю, что это X», и «я знаю, что это не X, но может быть представлен как один», «я знаю, что это не X, и не может быть действительно представлен как единое целое». , но все равно дай мне X. " Я мог видеть полезность для double-to-intброска, который потерпел бы неудачу, если doubleбы не представлял точное значение, которое могло бы поместиться в Int32, но иметь (int)-1.5выход -1 просто ужасно.
Суперкат
@supercat Да, но дизайн языка не так прост, хотя, как мы все знаем, взглянем на множество компромиссов, связанных с C # nullables. Единственное известное противоядие - это регулярное чтение C # в редакциях Depth по мере их появления :) К счастью, я больше обеспокоен пониманием нюансов F # в наши дни, и это гораздо более вменяемое во многих этих вопросах.
Рубен Бартелинк
@RubenBartelink: Мне не совсем ясно, какие именно проблемы должны были решать типы, допускающие обнуляемость, но я думаю, что в большинстве случаев было бы лучше иметь объект MaybeValid<T>с двумя открытыми полями IsValidи Valueкакой код можно было бы использовать по своему усмотрению. Это позволило бы, например MaybeValid<TValue> TryGetValue(TKey key) { var ret = default(MaybeValid<TValue>); ret.IsValid = dict.TryGetValue(key, out ret.Value); return ret; }. Это не только сэкономит как минимум две операции копирования по сравнению с ним Nullable<T>, но также может быть полезно для любого типа - Tне только для классов.
суперкат
2

Я считаю, что это asключевое слово можно считать более элегантно выглядящей версией dynamic_castиз C ++.

Эндрю Гаррисон
источник
Я бы сказал, что прямое приведение в C # больше похоже dynamic_castна C ++.
P Daddy
я думаю, что прямое приведение в C # более эквивалентно static_cast в C ++.
Эндрю Гаррисон
2
@Ruben Bartelink: возвращает только ноль с указателями. С ссылками, которые вы должны использовать, когда это возможно, он выбрасывает std::bad_cast.
P Daddy
1
@ Andrew Garrison: не static_castвыполняет проверку типов во время выполнения. В C # подобного не происходит.
P Daddy
К сожалению, я не знал, что вы могли бы даже использовать приведение к ссылкам, поскольку я использовал их только в указателях, но P Daddy абсолютно прав!
Эндрю Гаррисон
1

Вероятно, он более популярен не по техническим причинам, а только потому, что его легче читать и он более интуитивно понятен. (Не говорю, что это лучше, просто пытаясь ответить на вопрос)

JLA
источник
1

Одна из причин использования «как»:

T t = obj as T;
 //some other thread changes obj to another type...
if (t != null) action(t); //still works

Вместо (плохой код):

if (obj is T)
{
     //bang, some other thread changes obj to another type...
     action((T)obj); //InvalidCastException
}
Rauhotz
источник
2
Если у вас есть условия гонки, то у вас есть большие проблемы (но согласитесь, это хороший пример, чтобы пойти с другими, так что +1
Рубен Бартелинк
-1 как это увековечивает ошибку. Если другие потоки могут изменять тип объекта obj, у вас все еще есть проблемы. Утверждение "// все еще работает" очень неудобно для выполнения истины, поскольку t будет использоваться в качестве указателя на T, но оно указывает на память, которая больше не является T. Ни одно из решений не будет работать, когда другой поток изменит тип obj, пока выполняется действие (t).
Стивен С. Сталь
5
@Stephen C. Steel: Вы, кажется, в замешательстве. Изменение типа objозначало бы изменение самой objпеременной для хранения ссылки на другой объект. Это не изменит содержимое памяти, в которой находится объект, на который изначально ссылается obj. Этот исходный объект останется неизменным, а tпеременная все равно будет содержать ссылку на него.
P Daddy
infoq.com/news/2010/01/CDS-Dictionary
Рубен Бартелинк
1
@ P Папа - я думаю, что вы правы, и я был неправ: если obj был отскочил от объекта T к объекту T2, то t все равно будет указывать на старый объект T. Так как t по-прежнему ссылается на старый объект, он не может быть собран мусором, поэтому старый объект T останется действительным. Мои схемы детектора состояния гонки были обучены на C ++, где подобный код, использующий dynamic_cast, был бы потенциальной проблемой.
Стивен С. Сталь