“is” 测试链¶
ID: cs/chained-type-tests
Kind: problem
Security severity:
Severity: recommendation
Precision: high
Tags:
- changeability
- maintainability
- language-features
Query suites:
- csharp-security-and-quality.qls
长序列的类型测试主要用于根据变量的类型调度到不同的代码段,如本主题后面的示例所示。它们通常用于在不支持模式匹配的语言中模拟模式匹配。虽然它们确实可以用作调度方法,但它们有一些缺点
它们难以维护,因为很容易添加新的子类型并忘记修改代码中所有类型测试序列。
它们引入了对具体类的不必要的依赖关系。本来可以用接口编写的代码现在必须考虑所有特殊情况。
它们可能容易出错 - 如果您无意中在派生类型之前测试了基类型,则永远不会执行处理派生类型的代码。
它们的性能可能很差,因为它们实际上是线性时间迭代类型列表,依次检查每个类型。
建议¶
根据具体情况,有很多种可能的解决方案
多态性。这涉及向类型层次结构添加虚拟方法,并将要调用的代码段放在每个具体类的相关重写中。当您可以更改相关的类型层次结构并且正在实现的操作是相关类型应实现的功能的核心部分时,这是一种简洁的解决方案。如果选择这种方法,重要的是要注意不要引入不必要的依赖关系 - 如果操作依赖于本身依赖于类型层次结构的内容,那么您就不能在不创建依赖循环的情况下将操作移动到类型层次结构。
访问者模式。这涉及引入一个访问者接口,该接口包含类型层次结构中每个类型的
visit
方法,并在层次结构中每个类型中添加一个accept
方法,该方法将此类访问者作为其参数。每个类型的accept
方法在其上调用访问者的visit
方法this
。具体访问者然后实现接口并为每个特定类型执行必要的操作。当您可以更改相关的类型层次结构并且类型层次结构不应该知道正在实现的操作时(无论是出于依赖性原因还是因为它不是层次结构中类型的核心功能的一部分),这是一个很好的解决方案。如果您想在同一组类型上提供多个具有相同结构的操作,并且希望类型本身控制操作的结构方式(例如“使用顺序遍历访问此树并为每个节点执行必要的操作”),这也是明智的。缺点是基本的访问者模式是循环依赖的,并且所涉及的基础结构相对重量级。反射。这涉及根据方法参数之一的类型查找一组重载方法之一,并手动调用它。此选项永远不应该是首选,因为它会导致类型安全性的损失并且相当不整洁,但在某些情况下它可能是有意义的。特别是,当您无法更改相关的类型层次结构(例如,因为它属于第三方代码)并且您的代码必须使用不支持
dynamic
的 C# 版本(4.0 之前的版本)进行编译时,它很有用。使用
dynamic
。这涉及到将对象的类型(显式或隐式地)转换为dynamic
,以便对其执行动态解析的调用。这仅在 C# 4.0 及更高版本中可用。与反射一样,它会导致类型安全性的损失,尽管从语法的角度来看它更简洁一些。当您无法更改相关的类型层次结构,或者您希望避免像访问者模式这样重量级的解决方案时(您实际上是在牺牲一些类型安全性来换取可读性),这是一种有用的方法。
示例¶
此示例使用一系列链接的 is
语句,根据正在迭代的 Animal
的类型执行不同的操作。
class ChainedIs
{
interface Animal { }
class Cat : Animal { }
class Dog : Animal { }
public static void Main(string[] args)
{
List<Animal> animals = new List<Animal> { new Cat(), new Dog() };
foreach (Animal a in animals)
{
if (a is Cat)
Console.WriteLine("Miaow!");
else if (a is Dog)
Console.WriteLine("Woof!");
else
throw new Exception("Oops!");
}
}
}
下面的示例说明了多态性。
class ChainedIs
{
interface Animal
{
void Speak();
}
class Cat : Animal
{
public void Speak() { Console.WriteLine("Miaow!"); }
}
class Dog : Animal
{
public void Speak() { Console.WriteLine("Woof!"); }
}
public static void Main(string[] args)
{
List<Animal> animals = new List<Animal> { new Cat(), new Dog() };
foreach (var a in animals)
a.Speak();
}
}
下面是使用访问者模式的相同示例。如果动物叫声的概念应该与动物的概念分开,那么这是一个更好的解决方案。
class ChainedIs
{
interface Visitor
{
void Visit(Cat c);
void Visit(Dog d);
}
class SpeakVisitor : Visitor
{
public void Visit(Cat c) { Console.WriteLine("Miaow!"); }
public void Visit(Dog d) { Console.WriteLine("Woof!"); }
}
interface Animal
{
void Accept(Visitor v);
}
class Cat : Animal
{
public void Accept(Visitor v) { v.Visit(this); }
}
class Dog : Animal
{
public void Accept(Visitor v) { v.Visit(this); }
}
public static void Main(string[] args)
{
List<Animal> animals = new List<Animal> { new Cat(), new Dog() };
foreach (var a in animals)
a.Accept(new SpeakVisitor());
}
}
有关反射和 dynamic
使用的更多详细信息,请参阅参考资料。
参考资料¶
J. Albahari 和 B. Albahari,《C# 4.0 nutshell - 权威参考》,第 18 章和第 19 章,奥莱利媒体,2010 年。
R. Johnson、J. Vlissides、R. Helm 和 E. Gamma,《设计模式:可复用面向对象软件的元素》,Addison-Wesley 专业出版社,1994 年。