Chuyển nhượng trong một câu lệnh if


142

Tôi có một lớp Animal, và lớp con của nó Dog. Tôi thường thấy mình viết mã các dòng sau:

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

Đối với biến Animal animal;.

Có một số cú pháp cho phép tôi viết một cái gì đó như:

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

1
Điều đó có nghĩa là gì? Điều boolkiện sẽ là gì?
Kirk Woll

Không ai mà tôi biết. Bất kỳ lý do để không di chuyển Tên lên động vật?
AlG

22
Chỉ cần một lưu ý, mã như thường có thể là kết quả của việc phá vỡ một trong các Nguyên tắc RẮN . Các L - Liskov Substitution Nguyên tắc . Không nói sai khi làm những gì bạn đang làm mọi lúc, nhưng có thể đáng suy nghĩ.
ckittel

vui lòng lưu ý những gì @ckittel đang làm, có lẽ bạn không muốn làm điều này
khebbie

1
@Solo không , null! = falseTrong C #; C # chỉ cho phép các bool thực tế hoặc những thứ hoàn toàn có thể chuyển đổi thành các bool trong ifđiều kiện. Cả null và bất kỳ loại số nguyên nào đều có thể chuyển đổi hoàn toàn thành bools.
Roman Starkov

Câu trả lời:


323

Câu trả lời dưới đây được viết từ nhiều năm trước và được cập nhật theo thời gian. Kể từ C # 7, bạn có thể sử dụng khớp mẫu:

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

Lưu ý rằng dogvẫn còn trong phạm vi sau iftuyên bố, nhưng không được chỉ định chắc chắn.


Không, không có. Nó thành ngữ hơn để viết điều này mặc dù:

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

Cho rằng "như tiếp theo là" hầu như luôn được sử dụng theo cách này, có thể có ý nghĩa hơn khi có một toán tử thực hiện cả hai phần trong một lần. Điều này hiện không có trong C # 6, nhưng có thể là một phần của C # 7, nếu đề xuất khớp mẫu được triển khai.

Vấn đề là bạn không thể khai báo một biến trong phần điều kiện của ifcâu lệnh 1 . Cách tiếp cận gần nhất tôi có thể nghĩ là:

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

Điều đó thật khó chịu ... (Tôi vừa mới thử nó, và nó vẫn hoạt động. Nhưng làm ơn, làm ơn đừng làm điều này. Ồ, và bạn có thể tuyên bố dogsử dụng vartất nhiên.)

Tất nhiên bạn có thể viết một phương thức mở rộng:

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

Sau đó gọi nó bằng:

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

Ngoài ra, bạn có thể kết hợp cả hai:

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

Bạn cũng có thể sử dụng phương thức mở rộng mà không có biểu thức lambda theo cách sạch hơn vòng lặp for:

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

Sau đó:

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

1 Bạn có thể gán giá trị trong các ifcâu lệnh, mặc dù tôi hiếm khi làm như vậy. Điều đó không giống như khai báo các biến mặc dù. Nó không phải là quá bất thường đối với tôi để làm điều đó trong whilekhi đọc các luồng dữ liệu. Ví dụ:

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

Ngày nay, tôi thường thích sử dụng một trình bao bọc cho phép tôi sử dụng foreach (string line in ...)nhưng tôi xem ở trên là một mẫu khá thành ngữ. Nó thường không hay khi có các tác dụng phụ trong một điều kiện, nhưng các lựa chọn thay thế thường liên quan đến sao chép mã và khi bạn biết mẫu này, thật dễ dàng để có được đúng.


76
+1 để đưa ra câu trả lời và cũng cầu xin rằng OP không sử dụng nó. Ngay lập tức cổ điển.
ckittel

8
@Paul: Nếu tôi đang cố bán nó cho bất kỳ ai, tôi sẽ không khuyên họ không nên sử dụng nó. Tôi chỉ cho thấy những gì có thể .
Jon Skeet

12
@Paul: Tôi nghĩ rằng đó có thể là động lực đằng sau EVIL EVIL EVIL, nhưng tôi không tích cực.
Adam Robinson

18
Tôi đã thực hiện một phương pháp mở rộng tương tự (với một loạt các tình trạng quá tải) cách đây một thời gian và tôi đã gọi chúng AsEither(...), tôi nghĩ nó rõ ràng hơn một chút AsIf(...), vì vậy tôi có thể viết myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows()).
herzmeister

97
Đó là sự lạm dụng C # tốt nhất mà tôi từng thấy trong một thời gian. Rõ ràng bạn là một thiên tài xấu xa.
Eric Lippert

48

Nếu asthất bại, nó trở lại null.

Dog dog = animal as Dog;

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

Đầu tiên, cảm ơn bạn. Thứ hai, tôi muốn tạo biến chó trong phạm vi của ifcâu lệnh chứ không phải trong phạm vi bên ngoài.
michael

@Michael bạn không thể làm điều đó trong một câu lệnh if. Nếu phải có một kết quả bool không phải là một bài tập. Jon Skeet cung cấp một số kết hợp chung và lambda tốt đẹp mà bạn có thể xem xét là tốt.
Rodney S. Foley

ifcó thể có một kết quả bool một bài tập. Dog dog; if ((dog = animal as Dog) != null) { // Use Dog }nhưng điều đó vẫn giới thiệu các biến trong phạm vi bên ngoài.
Tom Mayfield

12

Bạn có thể gán giá trị cho biến, miễn là biến đó đã tồn tại. Bạn cũng có thể phạm vi biến để cho phép tên biến đó được sử dụng lại sau này trong cùng phương thức, nếu đó là một vấn đề.

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

giả định

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

được đầu ra:

Name is now Scopey
Flying

Mẫu phân công biến trong kiểm tra cũng được sử dụng khi đọc các khối byte từ các luồng, ví dụ:

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

Tuy nhiên, mẫu phạm vi biến được sử dụng ở trên không phải là mẫu mã đặc biệt phổ biến và nếu tôi thấy nó được sử dụng ở mọi nơi tôi sẽ tìm cách để cấu trúc lại nó.


11

Có một số cú pháp cho phép tôi viết một cái gì đó như:

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

?

Có khả năng sẽ có trong C # 6.0. Tính năng này được gọi là "biểu thức khai báo". Xem

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

để biết chi tiết.

Cú pháp đề xuất là:

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

Tổng quát hơn, tính năng được đề xuất là một khai báo biến cục bộ có thể được sử dụng như một biểu thức . Đây ifcú pháp chỉ là một hệ quả tốt đẹp của các tính năng tổng quát hơn.


1
Nhìn thoáng qua điều này có vẻ ít đọc hơn sau đó chỉ cần khai báo biến như bạn sẽ làm hôm nay. Bạn có thể biết tại sao tính năng đặc biệt này đã vượt qua thanh -100 điểm không?
asawyer

3
@asawyer: Đầu tiên, đây là một tính năng được yêu cầu rất thường xuyên. Thứ hai, các ngôn ngữ khác có phần mở rộng này thành "nếu"; gcc chẳng hạn cho phép tương đương trong C ++. Thứ ba, tính năng này tổng quát hơn là "nếu", như tôi đã lưu ý. Thứ tư, có một xu hướng trong C # kể từ C # 3.0 để tạo ra ngày càng nhiều thứ yêu cầu bối cảnh câu lệnh thay vì yêu cầu bối cảnh biểu thức; Điều này giúp với lập trình kiểu chức năng. Xem ghi chú thiết kế ngôn ngữ để biết thêm chi tiết.
Eric Lippert

2
@asawyer: Bạn được chào đón! Hãy tham gia thảo luận trên Roslyn.codeplex.com nếu bạn có thêm ý kiến. Ngoài ra, tôi sẽ nói thêm: Thứ năm, cơ sở hạ tầng Roslyn mới giảm chi phí cận biên cho nhóm thực hiện các loại tính năng thử nghiệm nhỏ này, điều đó có nghĩa là độ lớn của "điểm trừ 100" bị giảm. Nhóm đang tận dụng cơ hội này để khám phá các tính năng nhỏ hoàn hảo đã được yêu cầu từ lâu nhưng chưa bao giờ vượt quá rào cản -100 điểm trước đây.
Eric Lippert

1
Độc giả của những bình luận này đang bối rối về "điểm" mà chúng ta đang nói đến nên đọc bài viết trên blog của nhà thiết kế C # Eric Gunnerson về chủ đề này: blog.msdn.com/b/ericgu/archive/2004/01/12/57985. aspx . Đây là một sự tương tự; không có "điểm" thực tế nào được tính lên.
Eric Lippert

@asawyer: Tôi nghĩ tính năng này thực sự tỏa sáng trong các cuộc gọi đến Try*(ví dụ TryParse:). Tính năng này không chỉ biến các cuộc gọi như vậy thành một biểu thức duy nhất (như chúng phải là IMO), mà còn cho phép quét phạm vi của các biến đó. Tôi nhiệt tình về việc outtham số của một Tryphương thức được đặt trong phạm vi điều kiện của nó; điều này làm cho việc giới thiệu một số loại lỗi nhất định trở nên khó khăn hơn.
Brian

9

Một trong những phương pháp mở rộng tôi thấy mình viết và sử dụng thường xuyên * là

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

Mà có thể được sử dụng trong tình huống này như

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

Và sau đó namelà tên của con chó (nếu đó là một con chó), nếu không thì không.

* Tôi không biết nếu đây là biểu diễn. Nó chưa bao giờ đến như một nút cổ chai trong hồ sơ.


2
+1 cho ghi chú. Nếu nó chưa bao giờ xuất hiện như một nút cổ chai trong hồ sơ, thì đó là một dấu hiệu khá tốt cho thấy nó đủ hiệu quả.
Cody Grey

Tại sao bạn lại lấy defaultValue làm đối số và để người gọi quyết định tôi là gì thay vì quay lại mặc định (....)?
Trident D'Gao

5

Đi ngược lại hạt gạo ở đây, nhưng có lẽ bạn đang làm sai ở nơi đầu tiên. Kiểm tra loại đối tượng hầu như luôn luôn là mùi mã. Không phải tất cả Động vật, trong ví dụ của bạn, đều có Tên? Sau đó, chỉ cần gọi Animal.name, mà không kiểm tra xem đó có phải là một con chó hay không.

Ngoài ra, đảo ngược phương thức để bạn gọi một phương thức trên Động vật thực hiện một cái gì đó khác nhau tùy thuộc vào loại cụ thể của Động vật. Xem thêm: Đa hình.


4

Tuyên bố ngắn hơn

var dog = animal as Dog
if(dog != null) dog.Name ...;

3

Đây là một số mã bẩn bổ sung (không bẩn như Jon's, mặc dù :-)) phụ thuộc vào việc sửa đổi lớp cơ sở. Tôi nghĩ rằng nó nắm bắt được ý định trong khi có lẽ thiếu điểm:

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

Nếu bạn phải thực hiện nhiều lần như sau một (và sử dụng đa hình không phải là một tùy chọn), hãy xem xét sử dụng cấu trúc SwitchOnType .


3

Vấn đề (với cú pháp) không phải với phép gán, vì toán tử gán trong C # là một biểu thức hợp lệ. Thay vào đó, nó là với khai báo mong muốn như khai báo là tuyên bố.

Nếu tôi phải viết mã như vậy đôi khi tôi sẽ (tùy thuộc vào bối cảnh lớn hơn) viết mã như thế này:

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

Có những ưu điểm với cú pháp trên (gần với cú pháp được yêu cầu) bởi vì:

  1. Sử dụng dog bên ngoài các ifsẽ gây ra một lỗi biên dịch vì nó không được gán một giá trị ở nơi khác. (Đó là, đừng gán dogở nơi khác.)
  2. Cách tiếp cận này cũng có thể được mở rộng độc đáo thành if/else if/...(Chỉ có nhiều asyêu cầu để chọn một nhánh thích hợp; đây là trường hợp lớn mà tôi viết nó ở dạng này khi tôi phải.)
  3. Tránh trùng lặp is/as. (Nhưng cũng được thực hiện với Dog dog = ...hình thức.)
  4. Không khác gì "thành ngữ trong khi". (Chỉ cần không được mang đi: giữ điều kiện ở dạng nhất quán và đơn giản.)

Để thực sự cô lập dogvới phần còn lại của thế giới, một khối mới có thể được sử dụng:

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

Chúc mừng mã hóa.


Điểm số 1 mà bạn cung cấp là điều đầu tiên tôi nghĩ đến. Khai báo biến nhưng chỉ gán trong if. Biến sau đó không thể được tham chiếu từ bên ngoài nếu không có lỗi trình biên dịch - hoàn hảo!
Ian Yates

1

bạn có thể sử dụng một cái gì đó như thế

// Khai báo biến bool temp = false;

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

0

Một giải pháp EVIL khác với các phương thức mở rộng :)

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

Cá nhân tôi thích cách sạch sẽ:

Dog dog = animal as Dog;

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

0

Một câu lệnh if sẽ không cho phép điều đó, nhưng một vòng lặp for sẽ.

ví dụ

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

Trong trường hợp cách thức hoạt động không rõ ràng ngay lập tức thì đây là giải thích từng bước của quy trình:

  • Chó biến được tạo ra như chó loại và gán con vật biến được đúc cho Chó.
  • Nếu gán không thành công thì dog là null, điều này ngăn nội dung của vòng lặp for chạy, vì nó bị ngắt ngay lập tức.
  • Nếu gán thành công thì vòng lặp for chạy qua
    lặp.
  • Vào cuối vòng lặp, biến chó được gán một giá trị null, thoát ra khỏi vòng lặp for.

0
using(Dog dog = animal as Dog)
{
    if(dog != null)
    {
        dog.Name;    
        ... 

    }

}

0

IDK nếu điều này giúp được bất cứ ai nhưng bạn luôn có thể cố gắng sử dụng TryPude để gán biến của mình. Đây là một ví dụ:

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

Các tổng biến sẽ được khai báo trước nếu tuyên bố của bạn.


0

Tôi vừa nội tuyến câu lệnh if để tạo một dòng mã trông giống như những gì bạn quan tâm. Nó chỉ giúp nén một số mã và tôi thấy nó dễ đọc hơn, đặc biệt là khi lồng các bài tập:

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

Tôi biết tôi siêu lừa đảo đến bữa tiệc muộn, nhưng tôi đã hình dung rằng tôi sẽ tự khắc phục vấn đề nan giải này vì tôi chưa thấy nó ở đây (hoặc bất cứ nơi nào cho vấn đề đó).

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

Với điều này, bạn có thể làm những việc như:

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

LƯU Ý QUAN TRỌNG: Nếu bạn muốn sử dụng TryAs () bằng Giao diện, bạn PHẢI có giao diện đó kế thừa IAble.

Thưởng thức! 🙂

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.