在 if 语句中的赋值操作

172

我有一个类Animal,和它的子类Dog。 我经常编写以下代码:

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

对于变量 Animal animal;,是否有某种语法可以让我写出类似以下的代码:

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

我不知道有没有。把Name移到Animal上面有什么原因吗? - AlG
23
注意,像这样的代码经常是破坏SOLID原则之一的结果。L - 里氏替换原则。不是说你一直这样做是错的,但值得考虑。 - ckittel
请注意@ckittel正在做什么,你可能不想这样做。 - khebbie
2
@Solo,不,C#中的null并不等于false;C#只允许在if条件语句中使用实际的布尔值或可以隐式转换为布尔值的内容。既不能将空值(null)也不能将任何整数类型隐式转换为布尔值。 - Roman Starkov
现在我们有了C#7和新的语法。不过我要离开了,是时候学习本地的C++并使用一个更加精细的标准的语言了 ^-^ - Sergey.quixoticaxis.Ivanov
显示剩余5条评论
21个回答

374

以下答案是多年前写的,随着时间的推移进行了更新。从C# 7开始,您可以使用模式匹配:

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

请注意,在if语句之后,dog仍然在作用域内,但未被明确赋值。
没有,不过这样写更符合惯用语法:
Dog dog = animal as Dog;
if (dog != null)
{
    // Use dog
}

鉴于“as followed by if”几乎总是以这种方式使用,因此更有意义的做法可能是有一个运算符可以一次性执行两个部分。目前C#6中没有此功能,但如果实施模式匹配提案,则可能成为C#7的一部分。
问题在于您无法在if语句的条件部分中声明变量1。我能想到的最接近的方法是:
// EVIL EVIL EVIL. DO NOT USE.
for (Dog dog = animal as Dog; dog != null; dog = null)
{
    ...
}

这太恶心了...(我刚试过,它确实可以工作。但是,请不要这样做。哦,当然你可以使用var来声明dog。)

当然,您也可以编写扩展方法:

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 循环更清晰的方式使用扩展方法而无需使用 lambda 表达式:

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
}

1if语句中,你可以赋值,但我很少这样做。虽然如此,这与声明变量不同。但在读取数据流时,在while语句中这样做并不是特别不寻常的。例如:

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

最近我通常更喜欢使用一个包装器,它让我可以使用foreach (string line in ...),但我认为上述代码是一种相当惯用的模式。在条件语句中使用副作用通常不好,但其他选择通常涉及到代码重复,而且当你了解这个模式时,很容易做到正确无误。


82
点赞回答,同时恳请提问者不要使用。瞬间成为经典。 - ckittel
9
@Paul:如果我试图向任何人“销售”它,我不会强烈建议他们不要使用它。我只是展示了一下可能性。 - Jon Skeet
12
@Paul:我认为这可能是“EVIL EVIL EVIL”背后的动机,但我不确定。 - Adam Robinson
18
我曾经写过一个类似的扩展方法(带有多个重载),我称它们为 AsEither(...),我认为这比 AsIf(...) 更清晰,所以我可以写成 myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows()) - herzmeister
101
这是我最近看到的对C#最好的滥用。显然,你是个邪恶的天才。 - Eric Lippert
显示剩余23条评论

49
如果 as 失败,它会返回 null
Dog dog = animal as Dog;

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

首先,谢谢。其次,我想在 if 语句的作用域内创建 dog 变量,而不是在外部作用域中创建。 - michael
@Michael,在if语句中你不能这样做。if必须有一个布尔结果而不是一个赋值。Jon Skeet提供了一些不错的泛型和lambda组合,你可能也想考虑一下。 - Rodney S. Foley
if语句可以同时具有布尔结果和赋值功能。例如:Dog dog; if ((dog = animal as Dog) != null) { // 使用Dog },但这仍然会在外部作用域中引入变量。 - Tom Mayfield

13

只要变量已经存在,你就可以给它赋值。如果需要,在同一方法中可以将变量作用域限定以便稍后再次使用该变量名。

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:首先,这是一个非常常见的功能请求。其次,其他语言已经扩展了“if”的等价物;例如gcc允许在C++中使用。第三,正如我所指出的那样,这个功能比只有“if”更加通用。第四,自从C# 3.0以来,C#中越来越多的需要语句上下文的东西都需要表达式上下文;这有助于函数式编程。更多细节请参阅语言设计说明。 - Eric Lippert
2
@asawyer:不用谢!如果您有更多评论,请随时参与Roslyn.codeplex.com上的讨论。另外,我想补充的是:第五,新的Roslyn基础设施降低了实现团队进行这些小型实验性功能的边际成本,这意味着“减100”点的程度已经降低。团队正在利用这个机会探索一些完全合理的小功能,这些功能长期以来一直被要求,但以前从未超过“减100”点的障碍。 - Eric Lippert
2
对于那些对我们所谈论的“points”感到困惑的读者,可以阅读前C#设计师Eric Gunnerson在此主题上的博客文章:http://blogs.msdn.com/b/ericgu/archive/2004/01/12/57985.aspx。这只是一个比喻;实际上并没有在计算任何“points”。 - Eric Lippert
@asawyer:我认为这个特性在调用Try*(例如TryParse)时真的很出色。这个特性不仅将这样的调用变成了单个表达式(在我看来应该是这样的),而且还允许更清晰地作用于这些变量。我对Try方法的out参数被限定在其条件范围内感到热情洋溢;这使得引入某些类型的错误更加困难。 - Brian
1
注意:在2020年(C#9)中,不可能执行if ((var myLocal = myObj.myProp) != null ) { … myLocal … } - Grzegorz Dev

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
如果在分析中从未出现过瓶颈,那么对于该操作来说,这是一个相当不错的表现,值得给予肯定。 - Cody Gray
为什么要将defaultValue作为参数传递,并让调用者决定它的值,而不是回退到默认值(...)? - Trident D'Gao

5

这里可能需要打破常规,但也许您一开始就做错了。检查对象的类型几乎总是代码异味。在您的例子中,难道不是所有的动物都有一个名字吗?那么只需调用Animal.name,而无需检查它是否是狗。

或者,反转方法,使您在Animal上调用一个方法,具体取决于动物的具体类型。参见:多态性。


4
问题(与语法有关)不在于赋值,因为C#中的赋值运算符是有效表达式。相反,问题在于所需声明,因为声明是语句。
如果我必须编写这样的代码,我有时会(取决于更大的上下文)像这样编写代码:
Dog dog;
if ((dog = animal as Dog) != null) {
    // use dog
}

上述语法(与所请求的语法接近)有其优点,因为:
  1. if外使用dog将导致编译错误,因为它没有在其他地方分配值。(也就是说,不要在其他地方分配dog。)
  2. 这种方法还可以很好地扩展到if/else if/...(只需要选择适当的分支即可;当必须以这种形式编写时,这是一个大案例),而无需重复使用is/as。(但也可以使用Dog dog = ...形式完成。)
  3. 避免了is/as的重复使用。(但也可以使用Dog dog = ...形式完成。)
  4. 与“惯用while”没有区别。(只需不要过度使用:保持条件形式一致且简单。)

为了真正将dog与世界隔离开来,可以使用新块:

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

愉快地编程。


您所提供的第一个点是我想到的第一件事。在if语句中声明变量,但仅在其中进行赋值。这样,变量就不能在if之外引用,否则会出现编译器错误 - 完美! - Ian Yates

4

在C# 9.0和.NET 5.0中,你可以像这样使用 as 进行编写:

Animal animal;
if (animal as Dog is not null and Dog dog)
{
    //You can get here only if animal is of type Dog and you can use dog variable only
    //in this scope
}

这是因为在 if 语句中,将动物视为狗 与以下代码产生相同的结果:

animal is Dog ? (Dog)(animal) : (Dog)null

不为空部分检查上面语句的结果是否为 null。仅当这个语句为真时,它才创建类型为 Dog 的变量 dog,该变量不能为 null。

这个功能在 C# 9.0 中通过模式组合器引入,您可以在此处阅读更多信息: https://learn.microsoft.com/pl-pl/dotnet/csharp/language-reference/proposals/csharp-9.0/patterns3#pattern-combinators


4

简化语句

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

3

这里还有一些有点“脏”的代码(虽然没有Jon的那么脏 :-)),它依赖于修改基类。我认为它捕捉了意图,但也可能错过了重点:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接