Przypisanie w instrukcji if

142

Mam klasę Animali jej podklasę Dog. Często koduję następujące linie:

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

Dla zmiennej Animal animal;.

Czy jest jakaś składnia, która pozwala mi napisać coś takiego:

if (Dog dog = animal as Dog)
{    
    dog.Name;    
    ... 
}
Michael
źródło
1
Co to w ogóle oznaczałoby? Jaki byłby boolwarunek?
Kirk Woll,
Żadne, o których nie wiem. Czy jest jakiś powód, aby nie przenosić nazwy na zwierzę?
AlG
22
Uwaga, kod taki jak często może być wynikiem złamania jednej z SOLIDNYCH Zasad . L - Liskov Zmiana zasada . Nie mówię, że robienie tego, co robisz przez cały czas, jest złe, ale warto o tym pomyśleć.
ckittel
proszę zwróć uwagę na to, co robi @ckittel, prawdopodobnie nie chcesz tego robić
khebbie
1
@Solo no,! null= falseW C #; C # zezwala tylko na rzeczywiste boole lub rzeczy niejawnie konwertowane na bools w ifwarunkach. Ani wartości null, ani żaden z typów całkowitych nie są niejawnie konwertowane na bools.
Roman Starkov

Odpowiedzi:

323

Poniższa odpowiedź została napisana lata temu i aktualizowana z biegiem czasu. Począwszy od C # 7, możesz używać dopasowywania wzorców:

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

Zauważ, że dogpo ifinstrukcji nadal znajduje się w zakresie , ale nie jest ostatecznie przypisany.


Nie, nie ma. Bardziej idiomatyczne jest jednak napisanie tego:

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

Biorąc pod uwagę, że „jak następuje, jeśli” jest prawie zawsze używane w ten sposób, bardziej sensowne może być wprowadzenie operatora, który wykonuje obie części za jednym razem. To nie jest obecnie w C # 6, ale może być częścią C # 7, jeśli propozycja dopasowania wzorca zaimplementowano .

Problem polega na tym, że nie możesz zadeklarować zmiennej w części warunkowej ifinstrukcji 1 . Najbliższe podejście, jakie przychodzi mi do głowy, to:

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

To po prostu paskudne ... (Właśnie to wypróbowałem i działa. Ale proszę, nie rób tego. Och, i możesz zadeklarować dogużycievar oczywiście .)

Oczywiście możesz napisać metodę rozszerzającą:

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

Następnie zadzwoń za pomocą:

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

Alternatywnie możesz połączyć te dwa elementy:

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

Możesz także użyć metody rozszerzenia bez wyrażenia lambda w bardziej przejrzysty sposób niż pętla for:

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

Następnie:

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

1 Możesz przypisywać wartości w ifinstrukcjach, chociaż rzadko to robię. To nie to samo, co deklarowanie zmiennych. To nie jest strasznie nietypowe dla mnie zrobić w whilechociaż kiedy czyta strumienie danych. Na przykład:

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

W dzisiejszych czasach zwykle wolę używać opakowania, które pozwala mi używać, foreach (string line in ...)ale postrzegam powyższe jako dość idiomatyczny wzór. To zwykle nie miło mieć skutki uboczne w obrębie stanu, ale alternatywy obejmują zazwyczaj powielania kodu, a gdy wiesz, ten wzór jest to łatwe, aby uzyskać prawo.

Jon Skeet
źródło
76
+1 za udzielenie odpowiedzi, a także błaganie, aby PO jej nie używał. Błyskawiczna klasyka.
ckittel
8
@Paul: Gdybym próbował go komukolwiek sprzedać , zdecydowanie nie radziłbym mu go nie używać. Po prostu pokazuję, co jest możliwe .
Jon Skeet
12
@Paul: Myślę, że to mogła być motywacja EVIL EVIL EVIL, ale nie jestem pewien .
Adam Robinson
18
Zrobiłem podobną metodę rozszerzenia (z kilkoma przeciążeniami) jakiś czas temu i zadzwoniłem do nich AsEither(...), myślę, że jest to trochę jaśniejsze niż AsIf(...), więc mogę pisać myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows()).
herzmeister
97
To najlepsze nadużycie C #, jakie widziałem od jakiegoś czasu. Najwyraźniej jesteś złym geniuszem.
Eric Lippert
48

Jeśli się asnie powiedzie, zwraca null.

Dog dog = animal as Dog;

if (dog != null)
{
    // do stuff
}
Platinum Azure
źródło
Po pierwsze, dziękuję. Po drugie, chcę utworzyć zmienną psa w zakresie ifinstrukcji, a nie w zakresie zewnętrznym.
michael
@Michael, nie możesz tego zrobić w instrukcji if. Jeśli musi mieć wynik bool, a nie przypisanie. Jon Skeet zapewnia kilka fajnych kombinacji generycznych i lambda, które również możesz wziąć pod uwagę.
Rodney S. Foley
ifmoże mieć wynik bool i przypisanie. Dog dog; if ((dog = animal as Dog) != null) { // Use Dog }ale to wciąż wprowadza zmienną w zakresie zewnętrznym.
Tom Mayfield
12

Państwo może przypisać wartość do zmiennej, jak długo już istnieje zmienna. Możesz również określić zakres zmiennej, aby umożliwić późniejsze użycie tej nazwy zmiennej w tej samej metodzie, jeśli jest to problem.

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();
                // ...
            }
        }
    }
}

zarozumiały

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

pobiera dane wyjściowe:

Name is now Scopey
Flying

Wzorzec przypisania zmiennych w teście jest również używany przy odczytywaniu bloków bajtów ze strumieni, na przykład:

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

Zastosowany powyżej wzorzec zakresu zmiennych nie jest jednak szczególnie powszechnym wzorcem kodu i gdybym zobaczył, że jest używany w całym miejscu, szukałbym sposobu na jego refaktoryzację.

Rzemieślnik
źródło
11

Czy jest jakaś składnia, która pozwala mi napisać coś takiego:

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

?

Prawdopodobnie będzie w C # 6.0. Ta funkcja jest nazywana „wyrażeniami deklaracji”. Widzieć

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

dla szczegółów.

Proponowana składnia to:

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

Bardziej ogólnie, proponowana funkcja polega na tym, że jako wyrażenie można użyć lokalnej deklaracji zmiennej . Ta ifskładnia jest po prostu przyjemną konsekwencją bardziej ogólnej funkcji.

Eric Lippert
źródło
1
Na pierwszy rzut oka wydaje się to mniej czytelne niż zwykłe zadeklarowanie zmiennej, tak jak dzisiaj. Czy wiesz, dlaczego ta konkretna funkcja zdołała przekroczyć pasek -100 punktów?
asawyer
3
@asawyer: Po pierwsze, jest to bardzo często wymagana funkcja. Po drugie, inne języki mają rozszerzenie „if”; Na przykład gcc pozwala na odpowiednik w C ++. Po trzecie, jak zauważyłem, funkcja jest bardziej ogólna niż tylko „jeśli”. Po czwarte, istnieje trend w C # od czasu C # 3.0, aby tworzyć coraz więcej rzeczy, które wymagały kontekstu instrukcji, zamiast tego wymagają kontekstu wyrażenia; pomaga to w programowaniu w stylu funkcjonalnym. Więcej szczegółów można znaleźć w uwagach do projektowania języka.
Eric Lippert,
2
@asawyer: Nie ma za co! Jeśli masz więcej komentarzy, możesz wziąć udział w dyskusji na Roslyn.codeplex.com. Dodałbym również: po piąte, nowa infrastruktura Roslyn obniża koszty krańcowe zespołu wdrożeniowego związane z wykonywaniem tego rodzaju drobnych funkcji eksperymentalnych, co oznacza, że ​​wielkość punktów „minus 100” jest zmniejszona. Zespół korzysta z tej okazji, aby zbadać całkiem przyzwoite małe funkcje, o które od dawna prosili, ale nigdy wcześniej nie przekroczyły bariery -100 punktów.
Eric Lippert,
1
Czytelnicy tych komentarzy, którzy są zdezorientowani co do „punktów”, o których mówimy, powinni przeczytać wpis na blogu byłego projektanta C # Erica Gunnersona na ten temat: blogs.msdn.com/b/ericgu/archive/2004/01/12/57985. aspx . To jest analogia; nie ma faktycznych zliczanych „punktów”.
Eric Lippert
@asawyer: Myślę, że ta funkcja naprawdę wyróżnia się w wywołaniach Try*(np TryParse.). Ta funkcja nie tylko sprawia, że ​​takie wywołania są jednym wyrażeniem (tak powinno być, IMO), ale także pozwala na czystsze określanie zakresu takich zmiennych. Jestem entuzjastycznie nastawiony do tego, aby outparametr Trymetody był ograniczony do jej warunkowej; utrudnia to wprowadzanie pewnych typów błędów.
Brian
9

Jedną z metod rozszerzających, które często piszę i używam, jest *

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

Które mogą być użyte w tej sytuacji jako

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

I wtedy name imię psa (jeśli jest to pies), w przeciwnym razie jest puste.

* Nie mam pojęcia, czy to działa. Nigdy nie było wąskim gardłem w profilowaniu.

Greg
źródło
2
+1 dla notatki. Jeśli nigdy nie pojawił się jako wąskie gardło w profilowaniu, to całkiem dobry znak, że jest wystarczająco wydajny.
Cody Gray
Dlaczego miałbyś brać wartość defaultValue jako argument i pozwalać wywołującemu zdecydować, co mam zrobić, zamiast wracać do domyślnej (....)?
Trident D'Gao,
5

Idę pod prąd, ale może przede wszystkim robisz to źle. Sprawdzanie typu obiektu jest prawie zawsze zapachem kodu. Czy w twoim przykładzie wszystkie zwierzęta nie mają imienia? Następnie po prostu zadzwoń do Animal.name, bez sprawdzania, czy to pies, czy nie.

Alternatywnie, odwróć metodę, aby wywołać metodę na Animal, która robi coś inaczej w zależności od konkretnego typu Animal. Zobacz też: Polimorfizm.

fwielstra
źródło
4

Krótsza instrukcja

var dog = animal as Dog
if(dog != null) dog.Name ...;
jmogera
źródło
3

Oto dodatkowy brudny kod (choć nie tak brudny jak Jona :-)) zależny od modyfikacji klasy bazowej. Myślę, że oddaje intencję, ale być może mija się z celem:

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();
    }
}
James Ashley
źródło
3

Jeśli musisz wykonać wiele takich as-if, jeden po drugim (a użycie polimorfizmu nie jest opcją), rozważ użycie konstrukcji SwitchOnType .

Omer Raviv
źródło
3

Problem (ze składnią) nie dotyczy przypisania, ponieważ operator przypisania w C # jest prawidłowym wyrażeniem. Raczej jest z pożądaną deklaracją ponieważ deklaracje są instrukcjami.

Jeśli muszę napisać taki kod, czasami (w zależności od szerszego kontekstu) napiszę taki kod:

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

Powyższa składnia (która jest zbliżona do żądanej) ma zalety, ponieważ:

  1. Użycie dog poza the ifspowoduje błąd kompilacji, ponieważ nie ma przypisanej wartości w innym miejscu. (Oznacza to, że nie przydzielaj go doggdzie indziej.)
  2. To podejście można również ładnie rozszerzyć do if/else if/...(jest tylko tyle, asile potrzeba do wybrania odpowiedniej gałęzi; to duża sprawa której piszę to w tej formie, kiedy muszę).
  3. Pozwala uniknąć powielania is/as. (Ale także zrobione z Dog dog = ...formą.)
  4. Nie różni się od „idiomatycznej chwili”. (Tylko nie daj się ponieść emocjom: zachowaj warunek w spójnej i prostej formie).

Aby naprawdę odizolować się dogod reszty świata, można użyć nowego bloku:

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

Miłego kodowania.


źródło
Punkt 1, który oferujesz, jest pierwszą rzeczą, która przyszła mi do głowy. Zadeklaruj zmienną, ale przypisz ją tylko w if. W takim przypadku nie można odwoływać się do zmiennej z zewnątrz if bez błędu kompilatora - idealnie!
Ian Yates
1

możesz użyć czegoś takiego

// Zadeklaruj zmienną bool temp = false;

 if (previousRows.Count > 0 || (temp= GetAnyThing()))
                                    {
                                    }
NobDev
źródło
0

Kolejne ZŁE rozwiązanie z metodami rozszerzającymi :)

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

Osobiście wolę czysty sposób:

Dog dog = animal as Dog;

if (dog != null)
{
    // do stuff
}
Stefan Michev
źródło
0

Instrukcja if na to nie pozwala, ale pętla for tak.

na przykład

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

Jeśli sposób, w jaki to działa, nie jest od razu oczywisty, oto wyjaśnienie tego procesu krok po kroku:

  • Zmienny pies jest tworzony jako typ psa i przypisywany jest zmiennemu zwierzęciu, które jest rzutowane na Pies.
  • Jeśli przypisanie nie powiedzie się, pies jest pusty, co zapobiega uruchomieniu zawartości pętli for, ponieważ jest ona natychmiast przerywana.
  • Jeśli przypisanie powiedzie się, pętla for przechodzi przez
    iterację.
  • Pod koniec iteracji zmiennej psa przypisywana jest wartość null, która wyrywa się z pętli for.
WonderWorker
źródło
0
using(Dog dog = animal as Dog)
{
    if(dog != null)
    {
        dog.Name;    
        ... 

    }

}
WonderWorker
źródło
0

IDK, jeśli komuś to pomoże, ale zawsze możesz spróbować użyć TryParse do przypisania zmiennej. Oto przykład:

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

Łączna zmienna zostanie ogłoszony przed if.

Alejandro Garcia
źródło
0

Właśnie wstawiłem instrukcję if, aby utworzyć wiersz kodu, który wygląda jak to, co cię interesuje. Pomaga to tylko skompresować kod i okazało się, że jest bardziej czytelny, zwłaszcza podczas zagnieżdżania przypisań:

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
źródło
0

Wiem, że jestem super spóźniony na imprezę, ale pomyślałem, że opublikuję własne obejście tego dylematu, ponieważ jeszcze go tu nie widziałem (ani nigdzie w tej sprawie).

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

Dzięki temu możesz:

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

WAŻNA UWAGA: Jeśli chcesz używać TryAs () używając interfejsu, MUSISZ mieć ten interfejs dziedziczący IAble.

Cieszyć się! 🙂

Darian Lehmann-Plantenberg
źródło