Присваивание в операторе if

145

У меня есть класс Animalи его подкласс Dog. Я часто пишу следующие строки:

if (animal is Dog)
{
    Dog dog = animal as Dog;    
    dog.Name;    
    ... 
}

Для переменной Animal animal;.

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

if (Dog dog = animal as Dog)
{    
    dog.Name;    
    ... 
}
Майкл
источник
1
Что бы это вообще значило? В каком boolсостоянии?
Кирк Уолл
Я ничего не знаю. Есть ли причина не переносить Name на Animal?
AlG
22
Просто примечание, такой код часто может быть результатом нарушения одного из принципов SOLID . L - Лиск принцип замещения . Не говорю, что постоянно делать то, что вы делаете, неправильно, но, возможно, стоит подумать.
ckittel
пожалуйста, обратите внимание на то, что делает @ckittel, вы, вероятно, не захотите этого делать
khebbie
1
@Solo no null,! = falseВ C #; C # допускает только фактические bools или вещи, неявно конвертируемые в bools в ifусловиях. Ни значения NULL, ни какой-либо из целочисленных типов не могут быть неявно преобразованы в bools.
Роман Старков

Ответы:

329

Ответ ниже был написан много лет назад и со временем обновлялся. Начиная с C # 7, вы можете использовать сопоставление с образцом:

if (animal is Dog dog)
{
    // Use dog here
}

Обратите внимание, что dogэто все еще находится в области действия после ifоператора, но не определено.


Нет, нет. Однако более идиоматично написать это:

Dog dog = animal as Dog;
if (dog != null)
{
    // Use dog
}

Учитывая, что «как следует за условием» почти всегда используется таким образом, было бы целесообразнее использовать оператор, выполняющий обе части за один раз. В настоящее время этого нет в C # 6, но может быть частью C # 7, если реализовано предложение сопоставления с образцом .

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

// EVIL EVIL EVIL. DO NOT USE.
for (Dog dog = animal as Dog; dog != null; dog = null)
{
    ...
}

Это просто противно ... (Я только что попробовал, и это работает. Но, пожалуйста, не делайте этого. О, и вы, конечно, можете объявить dogиспользование var).

Конечно, вы можете написать метод расширения:

public static void AsIf<T>(this object value, Action<T> action) where T : class
{
    T t = value as T;
    if (t != null)
    {
        action(t);
    }
}

Затем вызовите его с помощью:

animal.AsIf<Dog>(dog => {
    // Use dog in here
});

В качестве альтернативы вы можете объединить два:

public static void AsIf<T>(this object value, Action<T> action) where T : class
{
    // EVIL EVIL EVIL
    for (var t = value as T; t != null; t = null)
    {
        action(t);
    }
}

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

public static IEnumerable<T> AsOrEmpty(this object value)
{
    T t = value as T;
    if (t != null)
    {
        yield return t;
    }
}

Затем:

foreach (Dog dog in animal.AsOrEmpty<Dog>())
{
    // use dog
}

1 Вы можете присваивать значения в ifоператорах, хотя я редко это делаю. Однако это не то же самое, что объявление переменных. Это не очень необычно для меня , чтобы сделать это в whileхотя , когда чтение потоков данных. Например:

string line;
while ((line = reader.ReadLine()) != null)
{
    ...
}

В наши дни я обычно предпочитаю использовать оболочку, которая позволяет мне использовать, foreach (string line in ...)но я рассматриваю вышесказанное как довольно идиоматический шаблон. Это , как правило , не приятно иметь побочные эффекты в пределах состояния, но альтернативы , как правило , включают дублирование кода, и если вы знаете эту модель легко получить право.

Джон Скит
источник
78
+1 за ответ, а также за то, что ОП его не использует. Мгновенная классика.
ckittel
8
@ Пол: Если бы я пытался продать его кому-нибудь, я бы не советовал им не использовать его. Я просто показываю, что возможно .
Джон Скит
12
@ Пол: Я думаю, что это могло быть мотивацией EVIL EVIL EVIL, но я не уверен.
Адам Робинсон
18
Некоторое время назад я сделал аналогичный метод расширения (с кучей перегрузок) и вызвал его AsEither(...), я думаю, он немного понятнее AsIf(...), поэтому я могу писать myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows()).
herzmeister
99
Это лучшее злоупотребление C #, которое я когда-либо видел. Ясно, что вы злой гений.
Эрик Липперт
48

В случае asнеудачи возвращается null.

Dog dog = animal as Dog;

if (dog != null)
{
    // do stuff
}
Платиновая лазурь
источник
Во-первых, спасибо. Во-вторых, я хочу создать переменную dog в области действия ifоператора, а не во внешней области.
Майкл
@Michael, вы не можете этого сделать в инструкции if. Оператор if должен иметь результат типа bool, а не присвоение. Джон Скит предлагает несколько хороших общих и лямбда-комбинаций, которые вы также можете рассмотреть.
Родни С. Фоли
ifможет иметь результат типа bool и присваивание. Dog dog; if ((dog = animal as Dog) != null) { // Use Dog }но это по-прежнему вводит переменную во внешнюю область видимости.
Том Мэйфилд
12

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

public void Test()
{
    var animals = new Animal[] { new Dog(), new Duck() };

    foreach (var animal in animals)
    {
        {   // <-- scopes the existence of critter to this block
            Dog critter;
            if (null != (critter = animal as Dog))
            {
                critter.Name = "Scopey";
                // ...
            }
        }

        {
            Duck critter;
            if (null != (critter = animal as Duck))
            {
                critter.Fly();
                // ...
            }
        }
    }
}

предполагая

public class Animal
{
}

public class Dog : Animal
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            Console.WriteLine("Name is now " + _name);
        }
    }
}

public class Duck : Animal
{
    public void Fly()
    {
        Console.WriteLine("Flying");
    }
}

получает вывод:

Name is now Scopey
Flying

Шаблон присвоения переменных в тесте также используется при чтении байтовых блоков из потоков, например:

int bytesRead = 0;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0) 
{
    // ...
}

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

Ремесленник
источник
12

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

if (Dog dog = animal as Dog) { ... dog ... }

?

Вероятно, будет в C # 6.0. Эта функция называется «выражением объявления». Увидеть

https://roslyn.codeplex.com/discussions/565640

для подробностей.

Предлагаемый синтаксис:

if ((var i = o as int?) != null) { … i … }
else if ((var s = o as string) != null) { … s … }
else if ...

В более общем плане предлагаемая функция состоит в том, что объявление локальной переменной может использоваться как выражение . Этот ifсинтаксис - просто приятное следствие более общей функции.

Эрик Липперт
источник
1
На первый взгляд это кажется менее читаемым, чем простое объявление переменной, как сегодня. Вы случайно не знаете, почему именно этой функции удалось преодолеть планку -100 пунктов?
asawyer
3
@asawyer: Во-первых, это очень часто запрашиваемая функция. Во-вторых, другие языки имеют это расширение на «если»; gcc, например, допускает аналог в C ++. В-третьих, как я заметил, эта функция носит более общий характер, чем просто «если». В-четвертых, в C #, начиная с C # 3.0, наблюдается тенденция делать все больше и больше вещей, требующих контекста оператора, а не контекста выражения; это помогает при программировании в функциональном стиле. См. Подробности в примечаниях к языку.
Эрик Липперт
2
@asawyer: Пожалуйста! Не стесняйтесь участвовать в обсуждении на Roslyn.codeplex.com, если у вас есть дополнительные комментарии. Кроме того, я бы добавил: В-пятых, новая инфраструктура Roslyn снижает предельные затраты для команды внедрения на выполнение такого рода небольших экспериментальных функций, что означает, что величина «минус 100» баллов уменьшается. Команда пользуется этой возможностью, чтобы изучить совершенно приличные мелкие функции, которые давно запрашивались, но ранее никогда не поднимались выше барьера -100.
Эрик Липперт
1
Читатели этих комментариев, которые не понимают, о каких «точках» мы говорим, должны прочитать сообщение в блоге бывшего разработчика C # Эрика Ганнерсона по этой теме: blogs.msdn.com/b/ericgu/archive/2004/01/12/57985. aspx . Это аналогия; Фактический подсчет «очков» не ведется.
Эрик Липперт
@asawyer: Я думаю, что эта функция действительно полезна при обращении к Try*(например, TryParse). Эта функция не только превращает такие вызовы в одно выражение (как и должно быть, IMO), но также позволяет более четко определить область видимости таких переменных. Я с энтузиазмом отношусь к тому, чтобы outпараметр Tryметода был ограничен его условной областью; это затрудняет внесение определенных типов ошибок.
Брайан
9

Один из методов расширения, который я часто пишу и использую *, это

public static TResult IfNotNull<T,TResult>(this T obj, Func<T,TResult> func)
{
    if(obj != null)
    {
        return func(obj);
    }
    return default(TResult);
}

Что можно было бы использовать в этой ситуации как

string name = (animal as Dog).IfNotNull(x => x.Name);

И затем nameимя собаки (если это собака), в противном случае - null.

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

Грег
источник
2
+1 за примечание. Если он никогда не был узким местом при профилировании, это хороший признак того, что он достаточно эффективен.
Коди Грей
Зачем брать значение defaultValue в качестве аргумента и позволять вызывающему абоненту решать, что ему делать, вместо того, чтобы возвращаться к значениям по умолчанию (....)?
Trident D'Gao
5

Здесь идет против течения, но, возможно, вы изначально делаете это неправильно. Проверка типа объекта почти всегда - это запах кода. Разве у всех животных в вашем примере нет имени? Затем просто вызовите Animal.name, не проверяя, собака это или нет.

В качестве альтернативы можно инвертировать метод, чтобы вы вызывали метод на Animal, который делает что-то по-другому в зависимости от конкретного типа Animal. См. Также: Полиморфизм.

Ктулху
источник
4

Краткое заявление

var dog = animal as Dog
if(dog != null) dog.Name ...;
Jmogera
источник
3

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

class Animal
{
    public Animal() { Name = "animal";  }
    public List<Animal> IfIs<T>()
    {
        if(this is T)
            return new List<Animal>{this};
        else
            return new List<Animal>();
    }
    public string Name;
}

class Dog : Animal
{
    public Dog() { Name = "dog";  }
    public string Bark { get { return "ruff"; } }
}


class Program
{
    static void Main(string[] args)
    {
        var animal = new Animal();

        foreach(Dog dog in animal.IfIs<Dog>())
        {
            Console.WriteLine(dog.Name);
            Console.WriteLine(dog.Bark);
        }
        Console.ReadLine();
    }
}
Джеймс Эшли
источник
3

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

Омер Равив
источник
3

Проблема (с синтаксисом) не в присваивании, поскольку оператор присваивания в C # является допустимым выражением. Скорее, это с желаемым объявлением, поскольку объявления являются операторами.

Если мне нужно написать такой код, я иногда (в зависимости от более широкого контекста) буду писать такой код:

Dog dog;
if ((dog = animal as Dog) != null) {
    // use dog
}

У приведенного выше синтаксиса (который близок к запрошенному) есть свои достоинства, поскольку:

  1. Использование dog внеif приведет к ошибке компиляции , поскольку она не присваивается значение в другом месте. (То есть не назначать dogгде-либо еще.)
  2. Этот подход также можно красиво расширить до if/else if/...(Их столько, asсколько требуется для выбора подходящей ветки; это большой случай, когда я пишу его в этой форме, когда мне нужно.)
  3. Избегает дублирования is/as. (Но тоже с Dog dog = ...формой.)
  4. Не отличается от "идиоматики пока". (Только не увлекайтесь: сохраняйте условное выражение в последовательной и простой форме.)

Чтобы по-настоящему изолировать себя dogот остального мира, можно использовать новый блок:

{
  Dog dog = ...; // or assign in `if` as per above
}
Bite(dog); // oops! can't access dog from above

Удачного кодирования.


источник
Пункт №1, который вы предлагаете, - это первое, что пришло мне в голову. Объявите переменную, но назначьте ее только в if. Тогда на переменную нельзя будет ссылаться извне if без ошибки компилятора - отлично!
Ян Йетс
1

вы можете использовать что-то подобное

// Объявить переменную bool temp = false;

 if (previousRows.Count > 0 || (temp= GetAnyThing()))
                                    {
                                    }
NobDev
источник
0

Еще одно ЗЛОЕ решение с методами расширения :)

public class Tester
{
    public static void Test()
    {
        Animal a = new Animal();

        //nothing is printed
        foreach (Dog d in a.Each<Dog>())
        {
            Console.WriteLine(d.Name);
        }

        Dog dd = new Dog();

        //dog ID is printed
        foreach (Dog dog in dd.Each<Dog>())
        {
            Console.WriteLine(dog.ID);
        }
    }
}

public class Animal
{
    public Animal()
    {
        Console.WriteLine("Animal constructued:" + this.ID);
    }

    private string _id { get; set; }

    public string ID { get { return _id ?? (_id = Guid.NewGuid().ToString());} }

    public bool IsAlive { get; set; }
}

public class Dog : Animal 
{
    public Dog() : base() { }

    public string Name { get; set; }
}

public static class ObjectExtensions
{
    public static IEnumerable<T> Each<T>(this object Source)
        where T : class
    {
        T t = Source as T;

        if (t == null)
            yield break;

        yield return t;
    }
}

Я лично предпочитаю чистый способ:

Dog dog = animal as Dog;

if (dog != null)
{
    // do stuff
}
Стефан Мичев
источник
0

Оператор if этого не допустит, но цикл for позволит.

например

for (Dog dog = animal as Dog; dog != null; dog = null)
{
    dog.Name;    
    ... 
}

Если то, как это работает, не сразу очевидно, то вот пошаговое объяснение процесса:

  • Переменная собака создается как тип собаки, и ей присваивается переменная животное, приведенная к собаке.
  • Если присваивание не удается, тогда dog имеет значение null, что предотвращает запуск содержимого цикла for, поскольку он немедленно прерывается.
  • Если присвоение выполнено успешно, цикл for выполняет
    итерацию.
  • В конце итерации переменной dog присваивается значение null, которое выходит из цикла for.
WonderWorker
источник
0
using(Dog dog = animal as Dog)
{
    if(dog != null)
    {
        dog.Name;    
        ... 

    }

}
WonderWorker
источник
0

IDK, если это кому-то поможет, но вы всегда можете попробовать использовать TryParse для назначения своей переменной. Вот пример:

if (int.TryParse(Add(Value1, Value2).ToString(), out total))
        {
            Console.WriteLine("I was able to parse your value to: " + total);
        } else
        {
            Console.WriteLine("Couldn't Parse Value");
        }


        Console.ReadLine();
    }

    static int Add(int value1, int value2)
    {
        return value1 + value2;
    }

Общая переменная будет объявлена перед вашим если заявление.

Алехандро Гарсия
источник
0

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

var dog = animal as Dog; if (dog != null)
{
    Console.WriteLine("Parent Dog Name = " + dog.name);

    var purebred = dog.Puppy as Purebred; if (purebred != null)
    {
         Console.WriteLine("Purebred Puppy Name = " + purebred.Name);
    }

    var mutt = dog.Puppy as Mongrel; if (mutt != null)
    {
         Console.WriteLine("Mongrel Puppy Name = " + mutt.Name);
    }
 }
user1689175
источник
0

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

/// <summary>
/// IAble exists solely to give ALL other Interfaces that inherit IAble the TryAs() extension method
/// </summary>
public interface IAble { }

public static class IAbleExtension
{
    /// <summary>
    /// Attempt to cast as T returning true and out-ing the cast if successful, otherwise returning false and out-ing null
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="able"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    public static bool TryAs<T>(this IAble able, out T result) where T : class
    {
        if (able is T)
        {
            result = able as T;
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }

    /// <summary>
    /// Attempt to cast as T returning true and out-ing the cast if successful, otherwise returning false and out-ing null
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    public static bool TryAs<T>(this UnityEngine.Object obj, out T result) where T : class
    {
        if (obj is T)
        {
            result = obj as T;
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }
}

С его помощью вы можете делать такие вещи, как:

if (animal.TryAs(out Dog dog))
{
    //Do Dog stuff here because animal is a Dog
}
else
{
    //Cast failed! animal is not a dog
}

ВАЖНОЕ ПРИМЕЧАНИЕ. Если вы хотите использовать TryAs () с помощью интерфейса, вы ДОЛЖНЫ иметь этот интерфейс, наследующий IAble.

Наслаждайтесь! 🙂

Дариан Леманн-Плантенберг
источник