我有一个类Animal
,和它的子类Dog
。
我经常编写以下代码:
if (animal is Dog)
{
Dog dog = animal as Dog;
dog.Name;
...
}
对于变量 Animal animal;
,是否有某种语法可以让我写出类似以下的代码:
if (Dog dog = animal as Dog)
{
dog.Name;
...
}
我有一个类Animal
,和它的子类Dog
。
我经常编写以下代码:
if (animal is Dog)
{
Dog dog = animal as Dog;
dog.Name;
...
}
对于变量 Animal animal;
,是否有某种语法可以让我写出类似以下的代码:
if (Dog dog = animal as Dog)
{
dog.Name;
...
}
以下答案是多年前写的,随着时间的推移进行了更新。从C# 7开始,您可以使用模式匹配:
if (animal is Dog dog)
{
// Use dog here
}
if
语句之后,dog
仍然在作用域内,但未被明确赋值。
Dog dog = animal as Dog;
if (dog != null)
{
// Use dog
}
// 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
}
1 在if
语句中,你可以赋值,但我很少这样做。虽然如此,这与声明变量不同。但在读取数据流时,在while
语句中这样做并不是特别不寻常的。例如:
string line;
while ((line = reader.ReadLine()) != null)
{
...
}
最近我通常更喜欢使用一个包装器,它让我可以使用foreach (string line in ...)
,但我认为上述代码是一种相当惯用的模式。在条件语句中使用副作用通常不好,但其他选择通常涉及到代码重复,而且当你了解这个模式时,很容易做到正确无误。
AsEither(...)
,我认为这比 AsIf(...)
更清晰,所以我可以写成 myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows())
。 - herzmeisteras
失败,它会返回 null
。
Dog dog = animal as Dog;
if (dog != null)
{
// do stuff
}
if
语句的作用域内创建 dog
变量,而不是在外部作用域中创建。 - michaelif
语句可以同时具有布尔结果和赋值功能。例如:Dog dog; if ((dog = animal as Dog) != null) { // 使用Dog }
,但这仍然会在外部作用域中引入变量。 - Tom Mayfield只要变量已经存在,你就可以给它赋值。如果需要,在同一方法中可以将变量作用域限定以便稍后再次使用该变量名。
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)
{
// ...
}
然而,上面使用的变量作用域模式并不是一个特别常见的代码模式,如果我看到它在各个地方都被使用,我会寻找一种重构它的方法。
有没有一些语法可以让我编写类似以下的内容:
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
语法只是更一般特性的一个美好结果。
Try*
(例如TryParse
)时真的很出色。这个特性不仅将这样的调用变成了单个表达式(在我看来应该是这样的),而且还允许更清晰地作用于这些变量。我对Try
方法的out
参数被限定在其条件范围内感到热情洋溢;这使得引入某些类型的错误更加困难。 - Brianif ((var myLocal = myObj.myProp) != null ) { … myLocal … }
。 - Grzegorz Dev我经常写并使用的扩展方法之一*是
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。
*我不知道这是否高效。在性能剖析中从未出现瓶颈。
这里可能需要打破常规,但也许您一开始就做错了。检查对象的类型几乎总是代码异味。在您的例子中,难道不是所有的动物都有一个名字吗?那么只需调用Animal.name,而无需检查它是否是狗。
或者,反转方法,使您在Animal上调用一个方法,具体取决于动物的具体类型。参见:多态性。
Dog dog;
if ((dog = animal as Dog) != null) {
// use dog
}
if
外使用dog
将导致编译错误,因为它没有在其他地方分配值。(也就是说,不要在其他地方分配dog
。)if/else if/...
(只需要选择适当的分支即可;当必须以这种形式编写时,这是一个大案例),而无需重复使用is/as
。(但也可以使用Dog dog = ...
形式完成。)is/as
的重复使用。(但也可以使用Dog dog = ...
形式完成。)为了真正将dog
与世界隔离开来,可以使用新块:
{
Dog dog = ...; // or assign in `if` as per above
}
Bite(dog); // oops! can't access dog from above
愉快地编程。
在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
简化语句
var dog = animal as Dog
if(dog != null) dog.Name ...;
这里还有一些有点“脏”的代码(虽然没有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();
}
}
null
并不等于false
;C#只允许在if
条件语句中使用实际的布尔值或可以隐式转换为布尔值的内容。既不能将空值(null)也不能将任何整数类型隐式转换为布尔值。 - Roman Starkov