更好地理解C#泛型

31

我看了一些使用C#泛型的示例代码。什么时候应该使用它们?

所有这些示例都很复杂。我需要一个简单明了的示例,让我开始使用C#泛型。


8
请看这个问题 - Jeff Mattfield
请参见:http://www.csharp-station.com/Tutorials/Lesson20.aspx - kenorb
5个回答

56
一个非常简单的例子就是通用的 List<T> 类。它可以容纳任何类型的对象。例如,你可以声明一个字符串列表(new List<string>())或者动物列表(new List<Animal>()),因为它是通用的。
如果不能使用通用类,该怎么办呢?你可以使用 ArrayList 类,但不足之处在于它所包含的类型是对象。因此,当你遍历列表时,你必须将每个项目转换为其正确的类型(即 stringAnimal),这需要更多的代码并且有性能损失。此外,由于 ArrayList 包含对象,它不是类型安全的。你仍然可以将一个 Animal 添加到一个字符串的 ArrayList 中:
ArrayList arrayList = new ArrayList();
arrayList.Add(new Animal());
arrayList.Add("");

因此,当迭代ArrayList时,您必须检查类型以确保实例是特定类型的,这会导致代码质量不佳:

foreach (object o in arrayList)
{
  if(o is Animal)
    ((Animal)o).Speak();
}

使用通用的List<string>,这是不可能实现的:

List<string> stringList = new List<String>();
stringList.Add("Hello");
stringList.Add("Second String");
stringList.Add(new Animal()); // error! Animal cannot be cast to a string

3
能够给出一个简洁明了、不复杂的例子,值得点赞。干得好。 - Subby
2
现在这才是我们所说的基本解释,配有精彩的示例和实时代码片段。太棒了!点赞+1。 - Kings
实际上,在语法上迭代ArrayList/List<T>没有区别。事实上,您迭代ArrayList的代码将无法编译。区别在于语义,foreach语句为您引入了一个强制转换。详见:http://ericlippert.com/2013/07/22/why-does-a-foreach-loop-silently-insert-an-explicit-conversion/。 - Sriram Sakthivel
你是对的@SriramSakthivel,语法是错误的。我更新了我的回答。 - Razzie

7

总结其他答案的一些重点:

1)泛型使您能够编写“通用”代码(即,它将适用于多个类型)。如果您想要编写“通用”行为,并且需要使其对不同的数据类型起作用,则只需编写该代码一次。List的示例是一个很好的例子,您可以需要使用每种类型实例化的相同代码来创建客户、产品、订单、供应商等列表。

//  snippet
List<Customer> customers = new List<Customer>();
Customer thisCustomer = new Customer();
customers.Add(thisCustomer);

List<Order> orders = new List<Order>();
Order thatOrder = new Order();
orders.Add(thatOrder);

//  etc.

2) 令人惊讶的是,泛型仍然能够保证类型安全!因此,如果您尝试这样做,您将正确地收到一个错误:

//  continued for snippet above
Order anotherOrder = new Order();
customers.Add(anotherOrder);    //  FAIL!

你希望这是一个错误,这样以后你的客户处理代码就不必处理出现在客户列表中的虚假订单。


我希望我能像Unmesh一样说出这句话:“重复是万恶之源。” - Jay

3

重复是万恶之源。当你需要在不同类型的数据上执行相同的操作时,就会出现代码重复的情况。泛型允许你避免这种情况,它允许你编写针对“通用”类型的代码,并随后将其替换为特定的类型。

解决此问题的另一种方法是使用类型为“System.Object”的变量,可以向其中分配任何类型的对象。这种方法涉及值类型和引用类型之间的装箱和拆箱操作,会影响性能。而且类型转换会使代码变得不干净。

泛型在MSIL和CLR中得到支持,因此表现非常好。

你应该阅读关于泛型的这些文章 -

http://msdn.microsoft.com/en-us/library/512aeb7t(VS.80).aspx

http://msdn.microsoft.com/en-us/library/ms379564(VS.80).aspx#csharp_generics_topic1


4
有许多罪恶的根源,比如过早的优化,甚至是过早地泛化 ;) - Sebastian Ðymel

0
简而言之,泛型允许您编写可以与任何类型的对象一起使用的类,但无需将数据转换为Object。这样做有性能优势,但也使您的代码更易读、易于维护且更少出错。
在可能的情况下,您应始终使用泛型而不是.NET 1.1风格的类。

-1

我这里有一个使用案例,来自于LinkedIn Learning的讲师Matt Milner。它可能有点啰嗦,不像你要求的那么简单,但我发现它对于深入了解泛型为什么必要非常有用。

泛型覆盖值类型和引用类型(在C#中)

  • 值类型:bool、byte、char、decimal、double、enum、float、int、long、sbyte、short、struct、uint、ulong、ushort。
  • 引用类型:String、Arrays(即使它们的元素是值类型)、Class、Delegate。

第一部分:关于值类型

假设你有这个方法:

static void Swap(object first, object second)
{
    object temp = second;
    second = first;
    first = temp;
}

你可以这样使用它:

int x = 5, y = 7;
Swap(x, y);
System.Console.WriteLine($"X: {x} and Y: {y}");

输出结果为:

X: 5 and Y: 7

这里没有交换。为什么?因为:

  • int 是值类型。
  • 所以,当它们进入方法并出来时,它们具有不同的作用域。它们只是被“按值复制”,而不是“按引用复制”。
  • 注意:C# 允许将 int 类型视为对象,但这涉及到装箱和拆箱。请记住,这是在内存层面上的昂贵操作。

这个问题应该通过使用引用类型来解决吗?让我们试一试。

第二部分:关于引用类型

我们将使用一个自定义类,先前在某个库中定义:

var p1 = new Person
{
    FirstName = "Matt",
    LastName = "Milner"
};

var p2 = new Person
{
    FirstName = "Amanda",
    LastName = "Owner"
};

让我们交换它们。

Swap(p1, p2);
System.Console.WriteLine($"Person 1 is: {p1.FirstName}");

我们应该读取Amanda,但我们得到的是:
Person 1 is: Matt

为什么?通常我们会期望发生变化,因为类实例是按引用传递的。但这并不是事实,因为我们实际上是传递了地址的副本给temp实例。

如果我们使用ref修改方法呢?

第三部分:使用ref

static void Swap(ref object first, ref object second)
{
    object temp = second;
    second = first;
    first = temp;
}

这应该允许我们不仅更改对象的部分,还可以更改它们所指向的内容。

Swap(ref p1, ref p2);
System.Console.WriteLine($"Person 1 is: {p1.FirstName}");
  • 这样做行不通。为什么?
  • 因为你不能将Personref转换为Objectref。它们是完全不同的类型。
  • 在运行程序之前,编译器会向您返回一个编译错误。

第四部分:使用泛型

  • 这个问题可以通过使用泛型来解决。
  • 它们可以与任何给定类型(int、数组等)一起使用,而不会遇到这些问题。
static void Swap<T>(ref T first, ref T second)
{
    T temp = second;
    second = first;
    first = temp;
}

这里我们有一个`T`参数,泛型或“类型”参数。 当在方法中使用时,T完全替代了Object声明,并且允许告诉方法我们正在放置什么。就像这样:
Swap<Person>(ref p1, ref p2);
Swap<int>(ref x, ref y);

System.Console.WriteLine($"Person 1: {p1.FirstName}");
System.Console.WriteLine($"X: {x} and Y: {y}");

输出结果为:

Person 1 is: Amanda
X: 7 and Y: 5

总结

  • 对于引用类型和值类型:
static void Swap(object first, object second); // No swap
static void Swap(ref object first, ref object second); // No swap
static void Swap<T>(ref T first, ref T second); // Swap
  • 这指定了方法的动态类型。
  • 在编译时,调用代码提供/通知正在使用的类型。

注意:我仍然需要自己检查为什么引用类型会以这种方式表现,据我所知,这与对象内存地址是如何传递、引用和复制有关。在大多数情况下,我只是按照Matt Miller的解释进行了跟随。


Mappers是另一个很好的使用案例(尽管更复杂)。在Visual Studio中,您可以考虑AutoMapper。 - carloswm85

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