C语言中的参考类型和值类型

187 阅读20分钟

本教程将介绍C#编程语言中的不同类型。在C#中,每个类型都属于两类中的一类。它们是参考类型和值类型。参考类型和值类型在程序中的表现是完全不同的,所以了解这些不同类型的工作方式是很重要的。做到这一点的最好方法是简单地编写一些示例代码,也许创建一些单元测试,并在Visual Studio中检查结果。


C#引用类型

当程序员在C#中定义一个类时,该类就成为一个参考类型。在类和对象的教程中,我们已经创建了一个StockPortfolio类。因此,我们已经创建了一个引用类型当一个变量被类型化时,意味着在变量名称前有一个类型定义,它持有一个引用--也被称为指针--指向内存中的一个对象。它持有一个指向内存中对象的地址。可以有几个变量都指向同一个对象。理解这个问题的最好方法是在Visual Studio中简单地测试一些东西,我们很快就会这样做。

  • 变量存储对一个对象的引用
  • 多个变量可以指向同一个对象
  • 一个变量在它的生命周期内可以指向多个对象
  • 对象被分配到内存中使用 new

reference type diagram

为了测试这些概念,让我们重新审视一下StockPortfolio类,并在属性中添加一个 **Name**到我们的属性中,就像这样。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks
{
    public class StockPortfolio
    {
        public StockPortfolio()
        {
            stocks = new List<float>();
        }

        public StockStatistics ComputeStatistics()
        {
            StockStatistics stats = new StockStatistics();


            float sum = 0;
            foreach (float stock in stocks)
            {
                stats.HighestStock = Math.Max(stock, stats.HighestStock);
                stats.LowestStock = Math.Min(stock, stats.LowestStock);
                sum += stock;
            }

            stats.AverageStock = sum / stocks.Count;
            return stats;
        }

        public void AddStock(float stock)
        {
            stocks.Add(stock);
        }

        public string Name;

        private List<float> stocks;
    }
}

现在,我们可以回到Program.cs文件,创建一个新的StockPortfolio对象,并将其放在portfolio1变量中。然后,我们将把 portfolio1 赋值给 portfolio2。我们可以给portfolio1分配一个名字,然后把portfolio2的名字写到控制台。你认为会发生什么?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.UI.DataVisualization.Charting;

namespace Stocks
{
    class Program
    {
        static void Main(string[] args)
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            portfolio1.Name = "Warren Buffet's Portfolio";
            Console.WriteLine(portfolio2.Name);
        }
    }
}

当我们运行程序时,我们看到了这个结果。
console writeline output

这是怎么发生的呢?我们给第一个变量分配了一个名字,然而我们写出了第二个变量中的值,它似乎与变量1中的值相同!这是因为两个变量都指向一个变量。这是因为两个变量都指向内存中的同一个对象。
two variables point to same object


用单元测试来测试类型

现在我们知道了如何在visual studio中设置单元测试,我们可以针对C#中的类型设置一些测试,看看我们是否得到了我们期望的结果。单元测试不仅有利于测试你的应用程序代码,而且对于学习语言中的一个新主题,如你可能想使用的一个新的API或库,它们也是很好的。通过单元测试,你可以设计一些实验,然后运行测试,看看你是否得到了你所期望的。让我们给我们的Stocks.Test项目添加一个新的
add class to vs project

来吧,像这样给文件起个名字叫ReferenceTypeTests.cs。
naming new class in visual studio

一旦文件到位,我们就可以创建一个新的TestMethod,这是一个公共的无效返回方法。这个方法的名字是VariablesHoldAReference。用它们要测试或证明的东西来命名你的测试方法,是一个很好的做法。这个测试的目的是实例化一个新的StockPortfolio对象,并让两个变量指向同一个StockPortfolio对象。我们要断言这两个变量是指向同一个对象的。这基本上与我们上面所做的相同,但我们不是把结果写到控制台,而是使用测试框架的断言来给我们一个真或假的结果。
在这个新文件中,我们需要添加这些代码。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks.Test
{
    [TestClass]
    public class ReferenceTypeTests
    {
        [TestMethod]
        public void VariablesHoldAReference()
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            portfolio1.Name = "Warren Buffet's Portfolio";
            Assert.AreEqual(portfolio1.Name, portfolio2.Name);
        }
    }
}

现在,让我们运行测试,观察一下,是的,它通过了
passing unit test in visual studio

所以,看起来我们已经掌握了引用类型的窍门。继续学习的一个好方法是用不同的变量做不同的实验,根据你认为应该发生的事情写一些断言,然后运行测试,看看你的信念是否正确。


C#值类型

在C#中,如上所述,有两类类型。它们是引用类型,以及现在的值类型。在C#中,值类型并不持有一个指针,而是持有实际的值。整数和浮点数是C#中价值类型的两个例子。考虑一个变量 **foo**的变量,它是一个整数类型。当foo变量被用在一个方法中并被分配了一个值,例如50,这个值就被存储在foo变量的内存地址中。它不是一个引用或指针。现在,让我们假设我们有另一个整数名为 bar.我们给它分配的值是25。现在我们有两个不同的变量,直接持有两个不同的值。这与我们创建StockPortfolio引用类型并有两个变量指向同一个对象时的情况非常不同。


为什么是值类型?

值类型很重要,因为它们比引用类型更有效,在内存中分配的速度更快。创建一个对象比创建一个像整数这样的值类型需要更多的处理能力和内存,而且一个应用程序可能需要数百万个值类型的存在。严格使用引用类型会变得太昂贵。因此,值类型被使用,因为它们使用最少的内存,而且速度非常快。存储整个对象,或者说是引用,要密集得多。基元是典型的值类型,它们直接将自己存储在一个变量中。这些值类型通常是不可变的,这意味着你不能改变一个值类型的值。不过不要把这与给整数变量分配不同的值混为一谈。例如,我们可以给变量 **bar**一个不同的值。我们可以把它从25改为10,但25和10这两个值是不会改变的。25的值永远是25,10的值永远是10。我们可以创建一个单元测试来显示值类型是如何工作的,我们将其称为IntVariablesHoldAValue()。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks.Test
{
    [TestClass]
    public class ReferenceTypeTests
    {
        [TestMethod]
        public void IntVariablesHoldAValue()
        {
            int foo = 50;
            int bar = foo;

            foo = 25;
            Assert.AreNotEqual(foo, bar);
        }

        [TestMethod]
        public void VariablesHoldAReference()
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            portfolio1.Name = "Warren Buffet's Portfolio";
            Assert.AreEqual(portfolio1.Name, portfolio2.Name);
        }
    }
}

为了真正看清这一点,我们可以使用带有断点的调试器,一步步地执行程序,同时一步步地观察存储在变量中的值。首先,我们设置一个断点。要做到这一点,你只需点击左边的灰色区域,你会看到一个红点出现。这表明,当程序开始运行时,它将在这一点上停止,这样你就可以检查程序中的所有数值和状态。
how to set break point in visual studio


调试测试

现在,你可以通过点击Test->Debug->All Tests开始调试这个单元测试。这与调试标准代码不同,在调试测试代码时,它的工作方式有点不同。调试器启动后就停在断点处。注意指向右边的黄色小箭头。
program stopped at breakpoint


第一步进入

在这一点上,程序可以说是在时间上被冻结了。现在,我们可以通过使用Step Into功能,一步一步地移动程序。继续按F11键,向前移动。黄色小箭头现在向下移动一行,在自动窗格中,我们看到 **foo**变量现在已经存在,但还没有被分配一个值。
first step into


第二步进入

在第二个步骤中,黄色箭头再次向下移动,现在我们可以看到 **foo**确实有一个50的值--它现在已经被分配了。此外。 **bar**现在已经存在,但是还没有被分配一个值。
second step into


第三步进入

第三步进入使黄色箭头在程序中再下一行。所以你可以看到,当使用调试器和步入功能时,你可以准确地看到变量的状态是如何一步步更新的。在这个时候,程序中的 foobar变量的值都是50。请注意,我们是在foo = 25的那一行停止的。在自动窗格中foo仍然是50。这是因为这一行还没有被执行,它将在下一步执行。
3rd step into


第四步进入

第四步是让我们达到一个点,即 **bar**的值为50,但注意到 foo.它现在的值是25。所以你可以看到,我们可以在一个值型的变量中覆盖一个现有的值。
4th step into


第五步进入

在最后一次迭代中,断言已被运行,25不等于50是真的。
5th step into

所以对于值类型,我们知道以下几点。

  • 变量持有值
  • 没有指针,没有引用
  • 许多内置基元是值类型
  • int, double, float

结构关键字

在上面的章节中,我们应该很清楚C#中的值类型是什么。现在我们可以问,我们如何在程序中创建自己的值类型。答案是通过使用结构。结构定义看起来就像类定义。它们都有一个名字,一个开头和结尾的大括号,以及里面的一些代码。结构还可以使用访问修饰符,如public和internal。在大多数情况下,你会希望使用类而不是结构。不过,也有使用结构的情况,通常是当你需要写一个代表单个值的抽象时。这是一个比使用类更简化的场景。结构在包含少量数据时效果最好,因为值类型经常在内存中被复制。然而,最需要理解的关键是,Structs是值类型,与类不同,它是引用类型。下面是一个结构体的例子。

public struct StockTicker
{
    public decimal price;
    public string company;
    public string symbol;
}

枚举关键字

除了使用结构来创建C#中的值类型外,你还可以使用枚举。枚举适合于在应用程序中处理命名的常量。例如,假设我们有一个股票应用程序,我们需要确定一只股票是否在纳斯达克、纽约证券交易所或OTC市场交易。你可以对每个交易所进行分类,并给它分配一个数字。因此,纳斯达克将是1,纽约证券交易所是2,而OTC是3。问题是,在源代码中,这些数字变成了神奇的数字。你开始在代码中引用数字,却忘记了它们实际上代表什么。因此,相反,你可以创建一个枚举。考虑一下这个例子。

public enum TickerExchange
{
    Nasdaq = 1,
    NYSE,
    OTC
}

现在有了这个枚举,你就可以写像你在这个if语句中看到的代码。当使用枚举而不是神奇的数字时,它变得更加清晰和可读。

if(ticker.Exchange == TickerExchange.Nasdaq)
{
    Console.WriteLine("The stock trades on the Nasdaq");
}

你可能想知道为什么在枚举的定义中,只有纳斯达克指数被分配了一个值。这是因为枚举会自动为每个连续的枚举成员赋值。所以纽约证券交易所等于2,OTC等于3,然而我们需要手动分配的只是第一个值。

  • 一个枚举创建了一个值类型
  • 一组命名的常量
  • 底层数据类型默认为int

为了看到这一点,我们可以使用枚举做一个字符串比较。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks.Test
{
    [TestClass]
    public class ReferenceTypeTests
    {
        [TestMethod]
        public void IntVariablesHoldAValue()
        {
            int foo = 50;
            int bar = foo;

            foo = 25;
            Assert.AreNotEqual(foo, bar);
        }

        [TestMethod]
        public void VariablesHoldAReference()
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            portfolio1.Name = "Warren Buffet's Portfolio";
            Assert.AreEqual(portfolio1.Name, portfolio2.Name);
        }

        [TestMethod]
        public void StringComparisons()
        {
            string ticker1 = "AAPL";
            string ticker2 = "aapl";

            bool result = String.Equals(ticker1, ticker2, StringComparison.InvariantCultureIgnoreCase);
            Assert.IsTrue(result);
        }
    }
}

在上面突出显示的代码中,ticker1 指向字符串 "AAPL"。ticker2变量指向 "aapl",同样的代码,但小写。从这里我们可以做一个StringComparison,但忽略大小写。现在,StringComparison是一个枚举,IntelliSense会告诉你这一点。
StringComparison enum

注意,我们可以看到各种可用的StringComparisions。我们可以看到CurrentCulture、CurrentCultureIgnoreCase、InvariantCulture、InvariantCultureIgnoreCase、Ordinal和OrdinalIgnoreCase。现在来看看这个。我们可以右键单击StringComparison,然后选择 "转到定义"。
visual studio go to definition

这就会出现元数据视图,向我们展示这个枚举是如何定义的。我们看到每个选项都有一个与之相关的值。C#编译器希望得到这些StringComparison值中的一个。这是使用强类型和使用枚举而不是神奇数字的一个好处。
metadata stringcomparison enum

这段代码。

String.Equals(ticker1, ticker2, StringComparison.InvariantCultureIgnoreCase);

对程序员来说比这段代码更有意义。

String.Equals(ticker1, ticker2, 3);

只是为了搞笑,我们将运行所有的测试。这个新的测试是通过的。所以我们知道 "AAPL "和 "aapl "在做不区分大小写的比较时是相等的,并且忽略了文化。
stingcomparison test passing


在C#中,参数是通过值传递的

当你调用一个需要参数的方法时,你传递的变量中的值将被复制到作为方法参数的变量中。你所传递的总是一个拷贝,这意味着对于引用类型,你传递的是变量内的引用的拷贝。如果一个值类型被传递到方法中,它就是变量中的值的拷贝。


通过值传递引用类型

例如,如果我们的代码中有一个方法需要一个StockPortfolio类型的参数,那么该方法就会得到一个StockPortfolio对象的指针(或引用)的副本。这意味着调用代码和被调用的方法都有指向同一个对象的指针。让我们看看在Visual Studio中一个方法获得指针副本的例子。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks.Test
{
    [TestClass]
    public class ReferenceTypeTests
    {
        [TestMethod]
        public void IntVariablesHoldAValue()
        {
            int foo = 50;
            int bar = foo;

            foo = 25;
            Assert.AreNotEqual(foo, bar);
        }

        [TestMethod]
        public void VariablesHoldAReference()
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            portfolio1.Name = "Warren Buffet's Portfolio";
            Assert.AreEqual(portfolio1.Name, portfolio2.Name);
        }

        [TestMethod]
        public void StringComparisons()
        {
            string ticker1 = "AAPL";
            string ticker2 = "aapl";

            bool result = String.Equals(ticker1, ticker2, StringComparison.InvariantCultureIgnoreCase);
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void ReferenceTypesPassByValue()
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            GivePortfolioName(portfolio2);
            Assert.AreEqual("Paul Tudor Jones Portfolio", portfolio1.Name);
        }

        private void GivePortfolioName(StockPortfolio portfolio)
        {
            portfolio.Name = "Paul Tudor Jones Portfolio";
        }
    }
}

在上面强调的测试代码中,我们做的第一件事是创建一个新的对象,并把它放在portfolio1变量中。然后我们将portfolio1中的引用分配给portfolio2。我们现在有两个变量指向同一个对象。之后,调用GivePortfolioName方法。这个方法不是测试的一部分,但它为我们做了一些工作。它接收一个StockPortfolio参数并设置该StockPortfolio的名称。该方法将其设置为 "Paul Tudor Jones Portfolio"。在ReferenceTypesPassByValue()方法中,GivePortfolioName()被调用,并输入组合2。当GivePortfolioName()被调用时,portfolio2内的值被复制到参数中。 **portfolio**参数,该值是一个指针。这意味着在测试的运行过程中,有三个变量指向同一个StockPortfolio对象。 portfolio1, portfolio2,以及 portfolio.如果我们通过任何一个变量查看该对象,通过这些变量对StockPortfolio所做的任何改变都是可见的。换句话说,portfolio.Name、portfolio1.Name和portfolio2.Name都将是 "Paul Tudor Jones Portfolio"。

我们可以在调试器中看到,就在GivePortfolioName()方法被调用之前,我们有两个变量都指向一个StockPortfolio对象。
two variables one object in debugger

然而!在该方法运行后,我们可以看到在该方法运行后,看看组合1和组合2的名称属性。它们都显示为字符串 "Paul Tudor Jones Portfolio",因为它们都指向同一个对象。
setting object property using a method


按价值传递价值类型

现在,我们来看看一个测试,当参数是值类型时,参数是通过值传递的。是的有点令人困惑,但让我们看看这是如何工作的。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks.Test
{
    [TestClass]
    public class ReferenceTypeTests
    {
        [TestMethod]
        public void IntVariablesHoldAValue()
        {
            int foo = 50;
            int bar = foo;

            foo = 25;
            Assert.AreNotEqual(foo, bar);
        }

        [TestMethod]
        public void VariablesHoldAReference()
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            portfolio1.Name = "Warren Buffet's Portfolio";
            Assert.AreEqual(portfolio1.Name, portfolio2.Name);
        }

        [TestMethod]
        public void StringComparisons()
        {
            string ticker1 = "AAPL";
            string ticker2 = "aapl";

            bool result = String.Equals(ticker1, ticker2, StringComparison.InvariantCultureIgnoreCase);
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void ReferenceTypesPassByValue()
        {
            StockPortfolio portfolio1 = new StockPortfolio();
            StockPortfolio portfolio2 = portfolio1;

            GivePortfolioName(portfolio2);
            Assert.AreEqual("Paul Tudor Jones Portfolio", portfolio1.Name);
        }

        private void GivePortfolioName(StockPortfolio portfolio)
        {
            portfolio.Name = "Paul Tudor Jones Portfolio";
        }

        [TestMethod]
        public void ValueTypesPassByValue()
        {
            int num = 25;
            IncrementNumber(num);
            Assert.AreNotEqual(26, num);
        }

        private void IncrementNumber(int number)
        {
            number += 1;
        }
    }
}

当这个测试运行时,num的整数在ValueTypesPassByValue()方法中被设置为25。紧接着,我们调用IncrementNumber(),传入一个值的副本,*不是*一个指针的副本!这就大大改变了行为。这大大改变了行为。在IncrementNumber()方法中,25现在变成了26,但它是一个值的副本,所以无论在IncrementNumber()方法中发生什么,在ValueTypesPassByValue()中存在的原始数字都保持在25!因此,当我们断言26不等于num时,这是真的。希望这两个例子能说明,尽管在C#中所有的参数都是按值传递的,但根据传递的是参考类型还是值类型,结果可能是不同的!

  • 参数是 "按值 "传递的
  • 引用类型传递的是引用的一个副本
  • 值类型传递一个值的副本

值类型是不可改变的

值得注意的是,Value Types是不可变的。这只是一种花哨的说法,即你不能改变它们。这只是意味着一旦你创建了一个值,你就不能改变这个值。然而,不要被混淆,因为这并不意味着存储在变量中的值不能改变。变量之所以被称为变量,是因为里面的数据可以变化,但价值类型的实际价值不能改变。例如,你不能改变整数值25的值。值25永远是值25。不可变的值类型的一个有趣方面是DateTime值类型。考虑一下我们测试类中的以下新测试。

[TestMethod]
public void AddDaysToDateTime()
{
    DateTime date = new DateTime(2019, 1, 1);
    date = date.AddDays(1);

    Assert.AreEqual(2, date.Day);
}

上面的代码正在创建一个新的日期时间并将其存储在 **date**变量中。下面是该变量在被分配了DateTime后立即出现的内容。
C Sharp DateTime

这个DateTime的全部内容都是不可变的,一旦创建就不能改变。注意,Day属性的值是1,代表第一天。当我们调用date.AddDays(1)时,似乎Day应该被增加到2。然后我们做一个断言来验证数字2是否等于date.Day()。猜猜会发生什么。测试失败。
c sharp datetime is immutable

为什么会发生这种情况?原因是AddDays并没有改变底层的DateTime值。AddDays实际做的是返回一个新的DateTime实例。这意味着返回的实例是最新的,而不是原来的。因此,如果我们简单地修改代码,将返回的实例捕捉到一个变量中,代码就会通过。

[TestMethod]
public void AddDaysToDateTime()
{
    DateTime date = new DateTime(2019, 1, 1);
    date = date.AddDays(1);

    Assert.AreEqual(2, date.Day);
}

同样的事情也发生在C#中的字符串上。字符串变量是一个指向字符序列的引用。当把字符串传递给不同的方法时,传递一个引用是很好的,因为如果它是一个非常大的字符串,复制整个字符串的值会很昂贵。这意味着字符串实际上是一个引用类型,但由于它是不可变的,所以表现得像一个值类型。请看这个测试代码。

[TestMethod]
public void UppercaseString()
{
    string ticker = "nflx";
    ticker.ToUpper();

    Assert.AreEqual("NFLX", ticker);
}

看起来不错吧?我们有一个小写的 "NFLX",然后对它调用ToUpper()方法。然后我们断言 "NFLX "应该等于我们刚刚对 "nflx "调用ToUpper()方法的结果。再一次,这个测试失败了。这是因为尽管字符串是一个引用类型,但它的行为非常像一个值类型。我们期望一个全大写的NFLX,但我们得到了一个全小写的NFLX。这是因为像Trim、ToUpper和ToLower这样的String方法,并没有修改我所指向的字符串。它们不修改底层的值本身。相反,它们创建一个新的字符串并从方法中返回该字符串。因此,我们需要通过将新修改的字符串的引用分配到ticker变量中来捕捉这一点,像这样。

[TestMethod]
public void UppercaseString()
{
    string ticker = "nflx";
    ticker = ticker.ToUpper();

    Assert.AreEqual("NFLX", ticker);
}

数组引用类型

在C#中,数组是一种数据结构,用于存储多个对象或值的集合。数组本身始终是一个引用类型。还有一种类似的数据结构类型叫做List,它略有不同。数组有一个固定的大小,你必须在使用它之前指定。另一方面,当你向它添加新的项目时,列表就会自动增长。因此,如果你确切地知道你需要处理多少个值,就使用数组。如果你需要动态调整大小,因为你不知道你可能要处理多少个值,那就用列表。列表和数组都是0索引的,这意味着列表或数组中的第一个项目从索引0开始,就像大多数编程语言一样。

我们可以通过这个测试代码来了解数组是如何成为一个参考类型的。

[TestMethod]
public void UsingArrays()
{
    float[] stocks;
    stocks = new float[8];

    AddStocks(stocks);

    Assert.AreEqual(27.22f, stocks[5]);
}

private void AddStocks(float[] stocks)
{
    stocks[5] = 27.22f;
}

我们设置了一个存放浮点数的数组,它的大小为8。 然后,我们看到我们有一个名为AddStocks的方法,它需要一个对浮点数数组的引用作为参数。在这个方法中,我们说股票sub 5 = 27.22f。你可能会想,既然float是一个值类型,那么使用数组的调用方法是否真的会看到这个值被放入数组中?是的,因为数组是一种引用类型,尽管里面的东西是一个值类型,但这个数字是存储在数组里面的,使用数组的股票变量和作为AddStocks参数的股票变量,都是对同一个数组的引用。


C#中的引用类型和值类型摘要

  • 每种类型都是一种值类型或引用类型
  • 使用结构来创建一个值类型
  • 使用类来创建一个引用类型
  • 数组和字符串是引用类型
  • 字符串的行为就像一个值类型

在本教程中,我们看到C#中的每个类型都属于两类中的一类。每个类型要么是引用类型,要么是值类型。你可以用 struct关键字创建一个新的值类型。在大多数情况下,使用引用类型和 class关键字。我们还看到了一些关于字符串和数组的有趣概念,它们都是引用类型。字符串的行为像一个值类型,因为字符串是不可变的。你只能建立新的字符串,你不能修改现有的字符串。