C# 编程/变量
变量 用于存储值。更准确地说,一个变量 绑定 一个 对象(在术语的普遍意义上,即一个特定值)到一个标识符(变量的名称)以便以后可以访问该对象。例如,变量可以存储一个值以便以后使用
string name = "Dr. Jones";
Console.WriteLine("Good morning " + name);
在这个例子中,“name”是标识符,“Dr. Jones”是我们绑定到它的值。此外,每个变量都用显式的类型声明。只有类型与变量声明类型兼容的值才能绑定到(存储在)变量中。在上面的例子中,我们将“Dr. Jones”存储到一个名为 string
的类型变量中。这是一个合法的语句。但是,如果我们说 int name
= "Dr. Jones"
,编译器会抛出一个错误,告诉我们不能在 int
和 string
之间进行隐式转换。有一些方法可以做到这一点,但我们将在以后讨论它们。
C# 支持多个与变量的通用编程概念相对应的程序元素:字段、参数和局部变量。
字段,有时称为类级变量,是与类或结构体关联的变量。实例变量是与类或结构体实例关联的字段,而静态变量,用 static 关键字声明,是与类型本身关联的字段。字段也可以通过将它们设为常量 (const) 来与它们的类关联,这需要对常量值进行声明赋值,并防止随后对该字段进行更改。
每个字段的可见性为公共、受保护、内部、受保护的内部或私有(从最可见到最不可见)。
与字段类似,局部变量可以选择设置为常量 (const)。常量局部变量存储在程序集数据区域中,而非常量局部变量存储在(或引用自)堆栈上。因此,它们既有声明它们的函数或语句块的范围,也有其扩展。
参数是与函数关联的变量。
输入参数的值可以从调用者传递到函数的环境中,因此函数对该参数的更改不会影响调用者的变量的值,也可以通过引用传递,因此对变量的更改将影响调用者的变量的值。值类型(int、double、string)按“值传递”,而引用类型(对象)按“引用传递”。由于这是 C# 编译器的默认设置,因此不需要使用 '&',就像在 C 或 C++ 中一样。
输出参数不会复制其值,因此在函数环境中对变量值的更改会直接影响调用者环境中的值。在函数进入时,编译器将此类变量视为未绑定,因此在给它赋值之前引用输出参数是非法的。为了使函数能够编译,它还必须在函数中每个有效(非异常)代码路径中被赋值。
引用参数类似于输出参数,但它在函数调用之前绑定,并且不需要由函数赋值。
params 参数表示可变数量的参数。如果方法签名中包含一个params 参数,则params 参数必须是签名中的最后一个参数。
// Each pair of lines is what the definition of a method and a call of a
// method with each of the parameters types would look like.
// In param:
void MethodOne(int param1) // definition
MethodOne(variable); // call
// Out param:
void MethodTwo(out string message) // definition
MethodTwo(out variable); // call
// Reference param;
void MethodThree(ref int someFlag) // definition
MethodThree(ref theFlag) // call
// Params
void MethodFour(params string[] names) // definition
MethodFour("Matthew", "Mark", "Luke", "John"); // call
C# 中的每个类型要么是值类型,要么是引用类型。C# 拥有几种预定义(“内置”)类型,并允许声明自定义值类型和引用类型。
值类型和引用类型之间存在根本差异:值类型分配在堆栈上,而引用类型分配在堆上。
.NET 框架中的值类型通常是小型、常用的类型。使用它们的优势在于,该类型需要很少的资源才能由 CLR 启动和运行。值类型不需要在堆上分配内存,因此不会导致垃圾回收。但是,为了发挥作用,值类型(或从它派生的类型)应该保持较小 - 理想情况下应该低于 16 字节的数据。如果选择让值类型变得更大,建议不要将其传递给函数(这可能需要复制其所有字段),也不要将其从函数中返回。
虽然这听起来像一个有用的类型,但它确实有一些缺陷,在使用它时需要了解这些缺陷。
- 值类型在传递给函数之前总是被复制(内在地)。对这个新对象的更改不会反映到传递给函数的原始对象中。
- 值类型不需要调用它们的构造函数。它们会自动初始化。
- 值类型总是将其字段初始化为 0 或 null。
- 值类型永远不能被赋值为 null(但可以使用可空类型)。
- 值类型有时需要装箱(包装在一个对象中),允许它们的值像对象一样使用。
CLR 以非常不同的方式管理引用类型。所有引用类型都包含两个部分:一个指向堆的指针(包含该对象)以及对象本身。引用类型略微更重,因为幕后管理需要跟踪它们。然而,这对于在传递指针而不是复制值到/从函数的灵活性以及速度提升来说是一个小代价。
当使用构造函数初始化一个引用类型对象时,CLR 需要执行以下四个操作。
- CLR 计算在堆上保存对象所需的内存量。
- CLR 将数据插入到新创建的内存空间中。
- CLR 标记空间的结束位置,以便可以将下一个对象放置在那里。
- CLR 返回对新创建空间的引用。
每次创建对象时都会发生这种情况。但是,假设内存是无限的,因此需要进行一些维护 - 这就是垃圾收集器发挥作用的地方。
由于 C# 中的类型系统与其他符合 CLI 的语言统一,因此每个 C# 整型实际上都是 .NET Framework 中对应类型的别名。虽然别名的名称在 .NET 语言之间有所不同,但 .NET Framework 中的底层类型保持不变。因此,在其他 .NET Framework 语言编写的程序集中创建的对象可以绑定到 C# 变量,这些变量的类型是根据下面的转换规则可以转换到的任何类型。以下内容通过将 C# 代码与等效的 Visual Basic .NET 代码进行比较,说明了类型的跨语言兼容性。
// C#
public void UsingCSharpTypeAlias()
{
int i = 42;
}
public void EquivalentCodeWithoutAlias()
{
System.Int32 i = 42;
}
' Visual Basic .NET
Public Sub UsingVisualBasicTypeAlias()
Dim i As Integer = 42
End Sub
Public Sub EquivalentCodeWithoutAlias()
Dim i As System.Int32 = 42
End Sub
使用特定于语言的类型别名通常被认为比使用完全限定的 .NET Framework 类型名称更具可读性。
每个 C# 类型都对应于统一类型系统中的一个类型,这一事实使每个值类型在跨平台和编译器之间具有一致的大小。这种一致性是与其他语言(如 C)的重要区别,在 C 中,例如,一个long
仅保证至少与一个int
一样大,并且由不同的编译器以不同的大小实现。作为引用类型,从object
派生的类型的变量(即任何class
)不受一致大小要求的限制。也就是说,引用类型的尺寸,如System.IntPtr
,而不是值类型,如System.Int32
,可能因平台而异。幸运的是,很少需要知道引用类型的实际大小。
有两个预定义的引用类型:object
,它是System.Object
类的别名,所有其他引用类型都从它派生;以及string
,它是System.String
类的别名。C# 同样具有几个整型值类型,每个都是 .NET Framework 的System
命名空间中对应值类型的别名。预定义的 C# 类型别名公开了底层 .NET Framework 类型的函数。例如,由于 .NET Framework 的System.Int32
类型实现了一个ToString()
函数来将整数的值转换为其字符串表示形式,因此 C# 的int
类型公开了该函数。
int i = 97;
string s = i.ToString(); // The value of s is now the string "97".
同样,System.Int32
类型实现了Parse()
函数,因此可以通过 C# 的int
类型访问它。
string s = "97";
int i = int.Parse(s); // The value of i is now the integer 97.
统一类型系统通过将值类型转换为引用类型(装箱)以及将某些引用类型转换为其对应值类型(拆箱)的能力得到了增强。这也被称为强制转换。
object boxedInteger = 97;
int unboxedInteger = (int) boxedInteger;
但是,装箱和强制转换不是类型安全的:如果程序员混淆了类型,编译器不会生成错误。在以下简短示例中,错误非常明显,但在复杂的程序中,可能很难发现。如果可能,避免装箱。
object getInteger = "97";
int anInteger = (int) getInteger; // No compile-time error. The program will crash, however.
内置的 C# 类型别名及其等效的 .NET Framework 类型如下所示。
C# 别名 | .NET 类型 | 大小(位) | 范围 |
---|---|---|---|
sbyte | System.SByte | 8 | -128 到 127 |
byte | System.Byte | 8 | 0 到 255 |
short | System.Int16 | 16 | -32,768 到 32,767 |
ushort | System.UInt16 | 16 | 0 到 65,535 |
char | System.Char | 16 | 代码为 0 到 65,535 的 Unicode 字符 |
int | System.Int32 | 32 | -2,147,483,648 到 2,147,483,647 |
uint | System.UInt32 | 32 | 0 到 4,294,967,295 |
long | System.Int64 | 64 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
ulong | System.UInt64 | 64 | 0 到 18,446,744,073,709,551,615 |
C# 别名 | .NET 类型 | 大小(位) | 精度 | 范围 |
---|---|---|---|---|
float | System.Single | 32 | 7 位 | 1.5 x 10-45 到 3.4 x 1038 |
double | System.Double | 64 | 15-16 位 | 5.0 x 10-324 到 1.7 x 10308 |
decimal | System.Decimal | 128 | 28-29 个小数位 | 1.0 x 10-28 到 7.9 x 1028 |
C# 别名 | .NET 类型 | 大小(位) | 范围 |
---|---|---|---|
bool | System.Boolean | 32 | true 或 false,它们与 C# 中的任何整数无关。 |
object | System.Object | 32/64 | 与平台相关(指向对象的指针)。 |
string | System.String | 16*length | 没有特殊上限的 Unicode 字符串。 |
预定义类型可以聚合和扩展为自定义类型。
自定义值类型用struct 或 enum 关键字声明。同样,自定义引用类型 用 class 关键字声明。
尽管数组声明中包含维数,但没有包含每个维的大小。
string[] a_str;
但是,对数组变量(在变量使用之前)的赋值指定了每个维的大小。
a_str = new string[5];
与其他变量类型一样,声明和初始化可以组合起来。
string[] a_str = new string[5];
同样重要的是要注意,就像在 Java 中一样,数组是按引用传递的,而不是按值传递的。例如,以下代码片段成功地交换了整数数组中的两个元素。
static void swap (int[] a_iArray, int iI, int iJ)
{
int iTemp = a_iArray[iI];
a_iArray[iI] = a_iArray[iJ];
a_iArray[iJ] = iTemp;
}
可以在运行时确定数组大小。以下示例将循环计数器分配给无符号短整型数组元素。
ushort[] a_usNumbers = new ushort[234];
[...]
for (ushort us = 0; us < a_usNumbers.Length; us++)
{
a_usNumbers[us] = us;
}
从 C# 2.0 开始,可以在结构中包含数组。
using System;
namespace Login
{
class Username_Password
{
public static void Main()
{
string username,password;
Console.Write("Enter username: ");
username = Console.ReadLine();
Console.Write("Enter password: ");
password = Console.ReadLine();
if (username == "SomePerson" && password == "SomePassword")
{
Console.WriteLine("Access Granted.");
}
else if (username != "SomePerson" && password == "SomePassword")
{
Console.WriteLine("The username is wrong.");
}
else if (username == "SomePerson" && password != "SomePassword")
{
Console.WriteLine("The password is wrong.");
}
else
{
Console.WriteLine("Access Denied.");
}
}
}
}
根据预定义的转换规则、继承结构和显式强制转换定义,给定类型的数值可能可以或不可以显式或隐式转换为其他类型。
许多预定义的值类型都具有到其他预定义值类型的预定义转换。如果类型转换保证不会丢失信息,则转换可以是 *隐式* 的(即,不需要显式的 *强制转换*)。
可以将值隐式转换为它继承的任何类或它实现的接口。要将基类转换为从它继承的类,转换必须是显式的,以便转换语句能够编译。类似地,要将接口实例转换为实现它的类,转换必须是显式的,以便转换语句能够编译。在这两种情况下,如果要转换的值不是目标类型的实例或其任何派生类型,运行时环境都会抛出转换异常。
变量的范围和范围基于它们的声明。参数和局部变量的范围对应于声明的方法或语句块,而字段的范围与实例或类相关联,并且可能被字段的访问修饰符进一步限制。
变量的范围由运行时环境使用隐式引用计数和复杂的垃圾回收算法确定。