C#中的List<int>

10

我无法理解List<int>背后的逻辑,因为它违反了一些基本规则。

List<int>应该是值类型而不是引用类型。

  1. List<int>如果要在函数调用之间保持其值,则必须通过ref关键字传递。这意味着它显示了类似于int的值类型行为。
  2. 但是List<int>必须由new运算符初始化。此外,List<int>也可能为null。这暗示了引用类型的行为。

可空类型则不同,因为它不需要由new运算符进行初始化。

我有什么地方看错了吗?

编辑 -

我应该在原始问题中发布代码。但是它在这里 -

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            ListTest d = new ListTest();
            d.Test();
        }
    }

    class ListTest
    {
        public void  ModifyIt(List<int> l)
        {
            l = returnList();
        }

        public void Test()
        {
            List<int> listIsARefType = new List<int>();
            ModifyIt(listIsARefType);
            Console.WriteLine(listIsARefType.Count); // should have been 1 but is 0
            Console.ReadKey(true);
        }

        public List<int> returnList()
        {
            List<int> t = new List<int>();
            t.Add(1);
            return t;
        }
    }
}

3
你的第一点具体是什么意思?你能举个例子解释为什么需要使用“ref”吗? - Dirk Vollmar
关于你的代码,问题在于你正在分配一个新列表。外部代码看不到新列表 - 但如果你改变原始列表,它将看到这些更改。 - Anon.
@Anon => 你说的“外部代码看不到新列表”是什么意思?能详细说明一下吗? - Prashant
@Prashant:假设你有一所房子,还有一张明信片告诉你这所房子的地址。你复制了那张明信片,并把副本交给了别人。如果他们去到房子并改变它,其他人也会看到被改变的房子,但如果他们建造了一座新房子并在自己的明信片上涂鸦了新地址,那不会影响其他任何人。 - Anon.
@Prashant - 我认为@Anon试图描述的是,持有引用类型的变量只是在堆上内存中保留一个位置的引用。因此,在“ModifyIt”方法中,“l”最初指向与“listIsARefType”相同的位置,但随后通过调用“new List <int>()”更改其地址,并将元素添加到该列表中……“listIsARefType”仍然指向原始位置-仍然是一个空列表。好问题-花了我一段时间才明白!;-) - IanR
显示剩余3条评论
9个回答

30

列表应该是值类型而不是引用类型。

错误! int 是一个值类型。 List<int> 是一个引用类型。


如果是这样的话,为什么要在函数之间通过引用传递List<int>? - Prashant
4
如果您想更改引用本身(即将其设置为null或不同的列表),则可以这样做。但我通常不会构建需要这样做的代码。 - Joel Coehoorn
3
@Prashant:你不需要这样做。告诉你必须这样做的人是个白痴。ref关键字的作用是允许你将参数设置为完全不同的List<int>。但这并不像你认为的那么常见。 - cHao
5
可怜的老引用,被人误解了。 :) - Esteban Araya

7

我认为你在第一条中存在一个错误的假设。泛型List对象肯定是引用类型(在堆上,而不是栈上)。不知道为什么你认为必须通过ref传递。这会像应该的那样打印“2”:

namespace ConsoleApplication1 {
   class Program {
      static void Main(string[] args) {
         List<int> listIsARefType = new List<int>();
         ModifyIt(listIsARefType);
         ModifyIt(listIsARefType);
         Console.WriteLine(listIsARefType.Count); // 2!
         Console.ReadKey(true);
      }

      static void ModifyIt(List<int> l) {
         l.Add(0);
      }
   }
}

我认为需要注意的是,任何List<T>都是引用类型,因为它是从一个引用类型的基类型创建的。需要注意的是,列表中的项目可能不是引用类型,但它可能会变成引用类型。 - msarchet

6
您需要了解“按引用传递”、“按值传递”和“按值传递引用”的区别。
在您发布的代码示例中,您通过“值”传递了对List对象的“引用”。这意味着您可以更改由引用指向的对象,并且调用代码将看到这些更改。但是,引用本身是按值传递的,因此如果您更改引用以指向完全不同的对象,则调用代码将无法看到更改。
当您使用ref关键字时,您正在通过引用本身传递引用。这意味着您不仅可以更改引用所指向的对象,还可以更改引用本身。
考虑以下示例:
class Program
{
    static void Main()
    {
        int foo = 0;
        DoSomething1(foo);
        Console.WriteLine(foo); // Outputs 0.

        DoSomething1(ref foo);
        Console.WriteLine(foo); // Outputs 1.

        var bar = new List<int>();
        DoSomething2(bar);
        Console.WriteLine(bar.Count); // Outputs 1.

        DoSomething2(ref bar);
        Console.WriteLine(bar.Count); // Outputs 0.
    }

    // Pass by value.
    static void DoSomething1(int number)
    {
        // Can't modify the number!
        number++;
    }

    // Pass by value.
    static void DoSomething1(ref int number)
    {
        // Can modify the number!
        number++;
    }

    // Pass reference by value.
    static void DoSomething2(List<int> list)
    {
        // Can't change the reference, but can mutate the object.
        list.Add(25);
    }

    // Pass reference by reference.
    static void DoSomething2(ref List<int> list)
    {
        // Can change the reference (and mutate the object).
        list = new List<int>();
    }
}

2

List<int>确实是引用类型。但列表中包含的项目是值类型。

然而,可空类型被实现为结构体(struct Nullable<T> where T : struct),因此它们是值类型。你可以简单地写出这样的代码的原因是

int? i = 3;

没有使用 new 关键字的原因是上述语法会被编译器自动转换为以下代码:

Nullable<Int32> i = new Nullable<Int32>(3);

为了更好地理解值类型和引用类型语义之间的差异,我建议您阅读Jon Skeet在此主题上的文章,其中将为您提供许多有说明性的代码示例:

Jon Skeet: C#中的参数传递


2
不要把它看作是 List<int>,而是按照它写成的方式来看待它 List<t>
List 是一个泛型类。它不是一个结构体。它是一个能够处理值类型和引用类型的泛型类。

1

List 是一个泛型引用类型,但你正在使用它与值类型 int。但它仍然是一个引用类型。


1
除了其他答案中涉及的错误假设之外,您还说:

List<int>必须通过 new 操作符进行初始化...... 这意味着引用类型行为。

不,C#中的 new 操作符只是调用类型构造函数的语法。它用于引用类型和用户定义的值类型(结构体)。

0

List<int> 是一个引用类型。它不必作为引用传递。

列表中的对象类型有点像值类型,除了值类型对象可能最终会被装箱(转换为某种引用类型对象),所以一旦你理解了这一点,就可以忽略本段落。


请注意,List<int> 不需要装箱。只有当您将值类型放入 List<object> 中时才会涉及装箱。 - Dirk Vollmar
@0xA3:我不确定.NET在这方面做得如何。我知道Java在这方面很烦人,但它的整数只是试图让原始类型看起来像对象,而.NET的“原始类型”实际上是对象。 - cHao

0
在ModifyIt()方法中,当你写' l = returnList() '时,' l '现在指向内存中与Test()方法中的listIsARefType不同的位置。基本上通过写'l' '=' something,你已经断开了'l'和'listIsARefType'之间的链接。为了保持链接(即确保'l'和'listIsARefType'两个对象都指向内存中的同一位置),你需要仅处理'l'对象(例如通过调用对象上的函数),或者在ModifyIt()方法的参数中使用ref关键字。

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