>  기사  >  백엔드 개발  >  C#은 제네릭을 이해합니다.

C#은 제네릭을 이해합니다.

黄舟
黄舟원래의
2017-02-07 16:59:251089검색

소개

Visual C# 2.0의 가장 기대되는(아마도 가장 어려운) 기능 중 하나는 제네릭 지원입니다. 이 글에서는 제네릭을 사용하여 해결하는 문제의 종류, 제네릭을 사용하여 코드 품질을 향상시키는 방법, 제네릭을 두려워할 필요가 없는 이유에 대해 설명합니다.

제네릭이란 무엇입니까?

많은 사람들이 제네릭을 이해하기 어렵다고 생각합니다. 제네릭을 사용하여 해결하는 문제를 이해하기 전에 많은 이론과 예제를 제공받는 경우가 많기 때문이라고 생각합니다. 결과적으로 해결책은 있지만 해결책이 필요한 문제는 없습니다.

이 글에서는 이러한 학습 과정을 바꾸려고 노력할 것입니다. 제네릭은 어디에 사용됩니까? 대답은 다음과 같습니다. 제네릭이 없으면 형식이 안전한 컬렉션을 만드는 것이 어려울 것입니다.

C#은 유형 안전 언어입니다. 유형 안전을 통해 컴파일러는 프로그램이 실행 중일 때 잠재적인 오류를 발견하는 대신(신뢰할 수 없을 정도로 제품을 판매할 때 종종 발생함) 이를 발견할 수 있습니다. . 따라서 C#에서는 모든 변수에 정의된 형식이 있습니다. 해당 변수에 개체를 할당하면 컴파일러는 할당이 올바른지 확인하고 문제가 있는 경우 오류 메시지를 표시합니다.

.Net 버전 1.1(2003)에서는 컬렉션을 사용할 때 이 유형의 안전성이 손상됩니다. .Net 클래스 라이브러리에서 제공하는 모든 컬렉션 클래스는 기본 유형(Object)을 저장하는 데 사용되며 .Net의 모든 항목은 Object 기본 클래스에서 상속되므로 모든 유형을 컬렉션에 넣을 수 있습니다. 따라서 유형 감지가 전혀 없는 것과 같습니다.

설상가상으로 컬렉션에서 객체를 꺼낼 때마다 이를 올바른 유형으로 변환해야 합니다. 이 변환은 성능에 영향을 미치고 자세한 코드를 생성합니다. 변환을 수행하면 예외가 발생합니다). 게다가 컬렉션에 값 유형(예: 정수 변수)을 추가하면 정수 변수가 암시적으로 박싱되고(다시 성능이 저하됨) 컬렉션에서 제거하면 명시적인 언박싱이 수행됩니다. 다시 (또 다른 성능 저하 및 유형 변환).

박싱 및 언박싱에 대한 자세한 내용을 보려면 Trap 4를 방문하고 암시적 박싱 및 언박싱에 주의하세요.

간단한 선형 연결 리스트 만들기

이런 문제를 생생하게 전달하기 위해 최대한 간단한 선형 연결 리스트를 만들어 보겠습니다. 선형 연결 리스트를 한번도 만들어본 적이 없는 이 글을 읽고 있는 분들을 위해. 선형 연결 목록을 상자 체인(노드라고 함)으로 생각할 수 있습니다. 각 상자에는 일부 데이터와 체인의 다음 상자에 대한 참조가 포함되어 있습니다(물론 마지막 상자는 제외). 다음 상자는 NULL로 설정됩니다.)

간단한 선형 연결 목록을 만들려면 다음 세 가지 클래스가 필요합니다.

1. 데이터와 다음 노드에 대한 참조가 포함된 노드 클래스입니다.

2. LinkedList 클래스에는 연결된 목록의 첫 번째 노드와 연결된 목록에 대한 추가 정보가 포함됩니다.

3. LinkedList 클래스를 테스트하는 데 사용되는 테스트 프로그램입니다.

연결된 목록이 어떻게 작동하는지 확인하기 위해 연결 목록에 정수 유형과 직원 유형이라는 두 가지 유형의 개체를 추가합니다. Employee 유형은 회사의 직원에 대한 모든 정보를 포함하는 클래스로 생각할 수 있습니다. 데모 목적으로 Employee 클래스는 매우 간단합니다.

public class Employee{
private string name;
  public Employee (string name){
    this.name = name;
  }

  public override string ToString(){
   return this.name;
  }
}

이 클래스에는 직원의 이름을 나타내는 문자열 유형, 직원의 이름을 설정하는 생성자, 직원의 이름을 반환하는 ToString() 메서드만 포함되어 있습니다.

연결된 목록 자체는 여러 노드로 구성됩니다. 위에서 언급한 것처럼 이러한 메모에는 연결 목록의 다음 노드에 대한 참조와 데이터(정수 및 직원)가 포함되어야 합니다.

public class Node{
    Object data;
    Node next;

    public Node(Object data){
       this.data = data;
       this.next = null;
    }

    public Object Data{ 
       get { return this.data; }
       set { data = value; }
    }

    public Node Next{
       get { return this.next; }
       set { this.next = value; }
    }
}

생성자는 전용 데이터 멤버를 전달된 객체로 설정하고 다음 필드를 null로 설정합니다.

이 클래스에는 노드 유형 매개변수를 허용하는 Append 메소드도 포함되어 있습니다. 전달된 노드를 목록의 마지막 위치에 추가합니다. 프로세스는 다음과 같습니다. 먼저 현재 노드의 다음 필드를 검사하여 null인지 확인합니다. 그렇다면 현재 노드는 마지막 노드이고 현재 노드의 다음 속성은 전달된 새 노드를 가리킵니다. 이러한 방식으로 연결 목록 끝에 새 노드를 삽입합니다.

현재 노드의 다음 필드가 null이 아닌 경우 현재 노드가 연결 리스트의 마지막 노드가 아니라는 의미입니다. 다음 필드의 유형도 노드이므로 다음 필드의 Append 메서드(참고: 재귀 호출)를 호출하고 Node 매개 변수를 다시 전달하며 이는 마지막 노드를 찾을 때까지 계속됩니다.

public void Append(Node newNode){
    if ( this.next == null ){
       this.next = newNode;
    }else{
       next.Append(newNode);
   }
}

Node 클래스의 ToString() 메서드도 재정의되어 데이터의 값을 출력하고 다음 Node의 ToString() 메서드를 호출하는 데 사용됩니다(주석: 또 다른 재귀 호출).

public override string ToString(){
    string output = data.ToString();

    if ( next != null ){
       output += ", " + next.ToString();
    }

    return output;
}

이렇게 하면 첫 번째 Node의 ToString() 메서드를 호출하면 연결된 목록에 있는 모든 Node의 값이 출력됩니다.

LinkedList 클래스 자체에는 하나의 노드에 대한 참조만 포함됩니다. 이 노드는 연결된 목록의 첫 번째 노드이며 null로 초기화되는 HeadNode입니다.

public class LinkedList{
    Node headNode = null;
}

LinkedList 类不需要构造函数(使用编译器创建的默认构造函数),但是我们需要创建一个公共方法,Add(),这个方法把 data存储到线性链表中。这个方法首先检查headNode是不是null,如果是,它将使用data创建结点,并将这个结点作为headNode,如果不是null,它将创建一个新的包含data的结点,并调用headNode的Append方法,如下面的代码所示:

public void Add(Object data){
    if ( headNode == null ){
       headNode = new Node(data);
    }else{
       headNode.Append(new Node(data));
    }
}

为了提供一点集合的感觉,我们为线性链表创建一个索引器。

public object this[ int index ]{
    get{
       int ctr = 0;
       Node node = headNode;
       while ( node != null  &&ctr <= index ){
           if ( ctr == index ){
              return node.Data;
           }else{
              node = node.Next;
           }
           ctr++;
       }
    return null;
    }
}

最后,ToString()方法再一次被覆盖,用以调用headNode的ToString()方法。

public override string ToString(){
    if ( this.headNode != null ){
       return this.headNode.ToString();
    }else{
       return string.Empty;
    }
}

测试线性链表

我们可以添加一些整型值到链表中进行测试:

public void Run(){
    LinkedList ll = new LinkedList();
    for ( int i = 0; i < 10; i ++ ){
       ll.Add(i);
    }

    Console.WriteLine(ll);
    Console.WriteLine("  Done.Adding employees...");
}

如果你对这段代码进行测试,它会如预计的那样工作:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...

然而,因为这是一个Object类型的集合,所以你同样可以将Employee类型添加到集合中。

ll.Add(new Employee("John"));
ll.Add(new Employee("Paul"));
ll.Add(new Employee("George"));
ll.Add(new Employee("Ringo"));

Console.WriteLine(ll);
Console.WriteLine(" Done.");

输出的结果证实了,整型值和Employee类型都被存储在了同一个集合中。

0, 1, 2, 3, 4, 5, 6, 7, 8, 9
  Done. Adding employees...
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, John, Paul, George, Ringo
Done.

虽然看上去这样很方便,但是负面影响是,你失去了所有类型安全的特性。因为线性链表需要的是一个Object类型,每一个添加到集合中的整型值都被隐式装箱了,如同 IL 代码所示:

IL_000c:  box        [mscorlib]System.Int32
IL_0011:  callvirt   instance void ObjectLinkedList.LinkedList::Add(object)

同样,如果上面所说,当你从你的列表中取出项目的时候,这些整型必须被显式地拆箱(强制转换成整型),Employee类型必须被强制转换成Employee类型。

Console.WriteLine("The fourth integer is " +Convert.ToInt32(ll[3]));
Employee d = (Employee) ll[11];
Console.WriteLine("The second Employee is " + d);

这些问题的解决方案是创建一个类型安全的集合。一个 Employee 线性链表将不能接受 Object 类型;它只接受 Employee类的实例(或者继承自Employee类的实例)。这样将会是类型安全的,并且不再需要类型转换。一个整型的 线性链表,这个链表将不再需要装箱和拆箱的操作(因为它只能接受整型值)。

作为示例,你将创建一个 EmployeeNode,该结点知道它的data的类型是Employee。

public class EmployeeNode {
    Employee employeedata;
    EmployeeNode employeeNext;
}

Append 方法现在接受一个 EmployeeNode 类型的参数。你同样需要创建一个新的EmployeeLinkedList ,这个链表接受一个新的 EmployeeNode:

public class EmployeeLinkedList{
    EmployeeNode headNode = null;
}

EmployeeLinkedList.Add()方法不再接受一个 Object,而是接受一个Employee:

public void Add(Employee data){
    if ( headNode == null ){
       headNode = new EmployeeNode(data);}
    else{
       headNode.Append(new EmployeeNode(data));
    }
}

类似的,索引器必须被修改成接受 EmployeeNode 类型,等等。这样确实解决了装箱、拆箱的问题,并且加入了类型安全的特性。你现在可以添加Employee(但不是整型)到你新的线性链表中了,并且当你从中取出Employee的时候,不再需要类型转换了。

EmployeeLinkedList employees = new EmployeeLinkedList();
employees.Add(new Employee("Stephen King"));
employees.Add(new Employee("James Joyce"));
employees.Add(new Employee("William Faulkner"));
/* employees.Add(5);  // try toadd an integer - won&#39;t compile */
Console.WriteLine(employees);
Employee e = employees[1];
Console.WriteLine("The second Employee is " + e);

这样多好啊,当有一个整型试图隐式地转换到Employee类型时,代码甚至连编译器都不能通过!

但它不好的地方是:每次你需要创建一个类型安全的列表时,你都需要做很多的复制/粘贴 。一点也不够好,一点也没有代码重用。同时,如果你是这个类的作者,你甚至不能提前欲知这个链接列表所应该接受的类型是什么,所以,你不得不将添加类型安全这一机制的工作交给类的使用者---你的用户。

使用泛型来达到代码重用

解决方案,如同你所猜想的那样,就是使用泛型。通过泛型,你重新获得了链接列表的   代码通用(对于所有类型只用实现一次),而当你初始化链表的时候你告诉链表所能接受的类型。这个实现是非常简单的,让我们重新回到Node类:

public class Node{
    Object data;
    ...

注意到 data 的类型是Object,(在EmployeeNode中,它是Employee)。我们将把它变成一个泛型(通常,由一个大写的T代表)。我们同样定义Node类,表示它可以被泛型化,以接受一个T类型。

public class Node <T>{
    T data;
    ...

读作:T类型的Node。T代表了当Node被初始化时,Node所接受的类型。T可以是Object,也可能是整型或者是Employee。这个在Node被初始化的时候才能确定。

注意:使用T作为标识只是一种约定俗成,你可以使用其他的字母组合来代替,比如这样:

public class Node <UnknownType>{
    UnknownType data;
    ...

通过使用T作为未知类型,next字段(下一个结点的引用)必须被声明为T类型的Node(意思是说接受一个T类型的泛型化Node)。

    Node<T> next;

构造函数接受一个T类型的简单参数:

public Node(T data)
{
    this.data = data;
    this.next = null;
}

Node 类的其余部分是很简单的,所有你需要使用Object的地方,你现在都需要使用T。LinkedList类现在接受一个 T类型的Node,而不是一个简单的Node作为头结点。

public class LinkedList<T>{
    Node<T> headNode = null;

再来一遍,转换是很直白的。任何地方你需要使用Object的,现在改做T,任何需要使用Node的地方,现在改做Node8742468051c85b06f0a0af9e3e506b5c。下面的代码初始化了两个链接表。一个是整型的。

LinkedList<int> ll = new LinkedList<int>();

另一个是Employee类型的:

LinkedList<Employee> employees = new LinkedList<Employee>();

剩下的代码与第一个版本没有区别,除了没有装箱、拆箱,而且也不可能将错误的类型保存到集合中。

LinkedList<int> ll = new LinkedList<int>();
for ( int i = 0; i < 10; i ++ )
{
    ll.Add(i);
}

Console.WriteLine(ll);
Console.WriteLine(" Done.");

LinkedList<Employee> employees = new LinkedList<Employee>();
employees.Add(new Employee("John"));
employees.Add(new Employee("Paul"));
employees.Add(new Employee("George"));
employees.Add(new Employee("Ringo"));

Console.WriteLine(employees); 
Console.WriteLine(" Done.");
Console.WriteLine("The fourth integer is " + ll[3]);
Employee d = employees[1];
Console.WriteLine("The second Employee is " + d);

泛型允许你不用复制/粘贴冗长的代码就实现类型安全的集合。而且,因为泛型是在运行时才被扩展成特殊类型。Just In Time编译器可以在不同的实例之间共享代码,最后,它显著地减少了你需要编写的代码。


以上就是C#理解泛型的内容,更多相关内容请关注PHP中文网(www.php.cn)!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
이전 기사:C#에서 gRPC 사용다음 기사:C#에서 gRPC 사용