Призначення у виписці if


142

У мене є клас 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умова?
Кірк Волл

Жодного, що я знаю. Будь-яка причина не переміщувати ім’я до Animal?
AlG

22
Просто зауваження, подібний до коду, часто може бути результатом порушення одного з принципів SOLID . L - Ліски принцип заміщення . Не кажучи, що неправильно робити те, що ви робите постійно, але, можливо, варто задуматися.
ckittel

будь ласка, зверніть увагу на те, що робить @ckittel, ви, мабуть, не хочете цього робити
khebbie

1
@ Соло ні,! null= falseВ C #; C # дозволяє лише фактичні булі або речі, неявно перетворювані на булі в ifумовах. Ні нулі, ні будь-який з цілих типів неявно перетворюються на булі.
Роман Старков

Відповіді:


323

Відповідь нижче була написана років тому і оновлювалася з часом. Станом на 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 ...)але я розглядаю вище як досить ідіоматичну схему. Це , як правило , не приємно мати побічні ефекти в межах стану, але альтернативи , як правило , включають дублювання коду, і якщо ви знаєте цю модель легко отримати право.


76
+1 за відповідь, а також благає, що ОП її не використовує. Миттєва класика.
ckittel

8
@Paul: Якби я намагався продати комусь, я б не радив їм не користуватися цим. Я просто показую, що можливо .
Джон Скіт

12
@Paul: Я думаю, що це мотивація була позаду EVIL EVIL EVIL, але я не позитивний.
Адам Робінсон

18
Я зробив подібний метод розширення (з купою перевантажень) деякий час тому, і я зателефонував їм AsEither(...), я думаю, що це трохи зрозуміліше, ніж AsIf(...)я можу написати myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows()).
herzmeister

97
Це найкраще зловживання C #, яке я бачив за деякий час. Ясно, що ви злий геній.
Ерік Ліпперт

48

Якщо asне вдасться, він повертається null.

Dog dog = animal as Dog;

if (dog != null)
{
    // do stuff
}

По-перше, дякую. По-друге, я хочу створити змінну собаки в обсязі ifвисловлювання, а не в зовнішній області.
Майкл

@Michael ви не можете цього зробити в операторі if. Якщо має бути результат bool, а не призначення. Джон Скіт пропонує кілька приємних загальних та лямбда-комбінацій, які ви також можете врахувати.
Родні С. Фолі

ifможе мати результат булінгу та призначення. 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) 
{
    // ...
}

Шаблон змінного масштабування, який використовується вище, однак, не є особливо поширеною схемою коду, і якби я бачив, що він використовується в усьому світі, я шукав би спосіб його рефакторингу.


11

Чи є якийсь синтаксис, який дозволяє мені написати щось на кшталт:

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, якщо у вас є більше коментарів. Також я додам: по-п’яте, нова інфраструктура Росліна знижує граничні витрати для команди, яка реалізує такі невеликі експериментальні можливості, а це означає, що величина "мінус 100" балів знижується. Команда користується цією можливістю вивчити ідеально пристойні невеликі функції, які давно просили, але ніколи раніше не робили це вище бар'єру -100.
Ерік Ліпперт

1
Читачі цих коментарів, котрі плутаються з приводу того, про які "пункти" ми говоримо, повинні прочитати публікацію блогу колишнього дизайнера C # Еріка Ганнерсона на цю тему: blogs.msdn.com/b/ericgu/archive/2004/01/12/57985. асп . Це аналогія; фактичні "бали" не підраховуються.
Ерік Ліпперт

@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ім'я собаки (якщо це собака), інакше недійсне.

* Я не маю уявлення, чи це виконавець. Він ніколи не з'являвся як вузьке місце в профілюванні.


2
+1 для примітки. Якщо він ніколи не з'являвся вузьким місцем у профілюванні, це досить хороший знак того, що він є досить ефективним.
Коді Грей

Чому б ви взяли аргумент defaultValue як аргумент і дозволили абоненту вирішити, що це, я замість того, щоб повернутися до дефолту (....)?
Тризуб Д'Гао

5

Тут йдеш проти зерна, але, можливо, ти робиш це неправильно в першу чергу. Перевірка типу об’єкта майже завжди є кодовим запахом. Чи не всі тварини у вашому прикладі мають ім’я? Тоді просто зателефонуйте на Animal.name, не перевіряючи, чи це собака чи ні.

Крім того, інвертуйте метод так, щоб ви викликали метод на Animal, який робить щось по-іншому залежно від конкретного типу тварини. Дивіться також: Поліморфізм.



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

Якщо вам доведеться робити декілька таких як-ifs один за одним (а використання поліморфізму не є варіантом), подумайте про використання конструкції 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, яку ви пропонуєте, - це перше, що мені прийшло в голову. Оголосіть змінну, але призначте лише якщо. Змінна тоді не може бути посилана ззовні, якщо без помилки компілятора - ідеально!
Ian Yates

1

ви можете використовувати щось подібне

// Оголосити змінну bool temp = false;

 if (previousRows.Count > 0 || (temp= GetAnyThing()))
                                    {
                                    }

0

Ще одне рішення EVIL з методами розширення :)

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

Якщо спосіб його роботи не відразу очевидний, то ось покрокове пояснення процесу:

  • Собака, що змінюється, створюється як собака типу і присвоює змінну тварину, яку передають собаці.
  • Якщо призначення не вдається, собака є нульовою, що запобігає запуску вмісту циклу for, оскільки він негайно виривається з.
  • Якщо призначення вдалося, цикл for проходить через
    ітерацію.
  • В кінці ітерації змінній собаці присвоюється значення null, яке виривається з циклу for.


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

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.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.