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
)与其类关联,这需要在声明时赋值,并禁止后续更改字段的值。
每个字段都有其可见性:public
、protected
、internal
、protected
internal
或private
(从最可见到最不可见)。
本地变量
与字段类似,本地变量也可以是常量(const
)。常量本地变量存储在程序集的数据区域中,而非常量本地变量存储在堆栈中(或通过堆栈引用)。因此,它们的作用域和生命周期都限制在声明它们的方法或语句块内。
参数
参数是与方法相关联的变量。
-
in
参数:可以将值从调用者传递到方法中,但方法对参数的修改不会影响调用者的变量值;或者通过引用传递,这样方法对变量的修改将影响调用者的变量值。值类型(如int
、double
、string
)默认按值传递,而引用类型(如对象)默认按引用传递。由于这是C#编译器的默认行为,因此不需要像C或C++中使用&
。 -
out
参数:值不会被复制,因此在方法中修改变量的值会直接影响调用者环境中的值。编译器认为out
参数在方法入口时是未绑定的,因此在赋值之前引用out
参数是非法的。此外,在方法中必须为out
参数在每条有效(非异常)代码路径上赋值,否则方法无法编译。 -
引用参数(
ref
):与out
参数类似,但在方法调用之前就已经绑定,并且方法不需要为它赋值。 -
params
参数:表示可变数量的参数。如果方法签名中包含params
参数,params
参数必须是签名中的最后一个参数。
示例
以下是每种类型的参数的定义及调用示例:
// in参数
void MethodOne(int param1) // 定义
MethodOne(variable); // 调用
// out参数
void MethodTwo(out string message) // 定义
MethodTwo(out variable); // 调用
// 引用参数(ref)
void MethodThree(ref int someFlag) // 定义
MethodThree(ref theFlag); // 调用
// params参数
void MethodFour(params string[] names) // 定义
MethodFour("Matthew", "Mark", "Luke", "John"); // 调用
类型
在C#中,每种类型都是值类型或引用类型之一。C#提供了一些预定义(“内置”)类型,并允许声明自定义值类型和引用类型。
值类型与引用类型之间的一个根本区别在于:值类型分配在堆栈上,而引用类型分配在堆上。
值类型
.NET框架中的值类型通常是小型且常用的类型。使用值类型的好处是它们占用的资源非常少,因此CLR可以快速初始化。值类型无需在堆上分配内存,因此不会触发垃圾回收。然而,为了保持高效,值类型(或其派生类型)应尽量保持小型——理想情况下不超过16字节。如果将值类型设计得过大,建议不要将其传递给方法(因为这可能需要复制所有字段),或从方法返回。
尽管值类型很有用,但在使用时需了解以下特性和局限:
- 值类型在传递给方法之前会被自动复制。因此,对该新对象的更改不会反映到原始对象上。
- 值类型不需要调用其构造函数即可使用,它们会自动初始化。
- 值类型的字段总是初始化为
0
或null
。 - 值类型不能直接被赋值为
null
,但可以使用Nullable
类型实现此功能。 - 值类型有时需要被装箱(即包装在一个对象中),以便像对象一样使用其值。
引用类型
引用类型由CLR以完全不同的方式管理。所有引用类型由两部分组成:一个指向堆上对象的指针,以及对象本身。由于需要后台管理来跟踪引用类型,因此它们比值类型略微“重量级”。但这是为了传递指针而不是复制值的灵活性和速度所付出的较小代价。
当初始化一个引用类型的对象时(通过构造函数),CLR会执行以下四个操作:
- 计算对象在堆上所需的内存大小。
- 将数据插入新创建的内存空间。
- 标记该内存空间的结束位置,以便下一个对象可以放置。
- 返回指向新创建内存空间的引用。
这些操作每次创建对象时都会执行。由于假设内存是无限的,因此需要定期进行维护,而这正是垃圾回收器的职责。
整型类型
由于C#的类型系统与其他符合CLI(公共语言基础结构)标准的语言是统一的,每种整型类型实际上都是.NET框架中对应类型的别名。虽然别名在不同的.NET语言中可能有所不同,但底层类型在.NET框架中是一致的。因此,根据以下转换规则,使用.NET框架其他语言编写的程序集中的对象可以绑定到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框架的完全限定类型名称更具可读性。
C#中的每种类型都对应于统一类型系统中的一种类型,这确保了每种值类型在跨平台和编译器之间的大小一致性。这与其他语言(如C语言)形成鲜明对比,在C中,例如long
类型只保证至少与int
一样大,但在不同的编译器中实现的大小可能不同。
作为引用类型,派生自object
(即任意类)的变量不受一致大小要求的约束。例如,引用类型(如System.IntPtr
)的大小可能因平台而异,而值类型(如System.Int32
)则保持一致。幸运的是,通常无需了解引用类型的实际大小。
预定义类型
C#提供两种预定义的引用类型:
object
:这是System.Object
类的别名,所有其他引用类型都派生自它。string
:这是System.String
类的别名。
此外,C#还具有多种整型值类型,每种值类型都是.NET框架System
命名空间中对应值类型的别名。这些别名公开了底层.NET框架类型的方法。例如,由于.NET框架的System.Int32
类型实现了ToString()
方法来将整数值转换为其字符串表示形式,C#的int
类型也暴露了该方法:
int i = 97;
string s = i.ToString(); // 此时 s 的值是字符串 "97"。
同样,System.Int32
类型实现了Parse()
方法,因此可以通过C#的int
类型访问该方法:
string s = "97";
int i = int.Parse(s); // 此时 i 的值是整数 97。
统一类型系统的优势在于能够将值类型转换为引用类型(装箱),以及将某些引用类型转换为它们对应的值类型(拆箱)。这也被称为类型转换(casting)。
object boxedInteger = 97;
int unboxedInteger = (int)boxedInteger;
然而,装箱和类型转换并不是类型安全的:如果程序员混淆了类型,编译器不会生成错误。在以下简单示例中,错误非常明显,但在复杂的程序中可能很难发现。因此,应尽量避免装箱。
object getInteger = "97";
int anInteger = (int)getInteger; // 编译时不会报错,但程序运行时会崩溃。
内置C#类型别名及其对应的.NET框架类型
整数类型
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 | Unicode字符,代码范围为 0 到 65,535 |
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 10^38 |
double | System.Double | 64 | 15-16 位数字 | 5.0 x 10^-324 到 1.7 x 10^308 |
decimal | System.Decimal | 128 | 28-29 位小数 | 1.0 x 10^-28 到 7.9 x 10^28 |
其他预定义类型
C#别名 | .NET类型 | 大小(位) | 范围 |
---|---|---|---|
bool | System.Boolean | 32 | true 或 false ,与C#中的任何整数无关 |
object | System.Object | 32/64 | 平台相关(指向对象的指针) |
string | System.String | 16*长度 | 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
数组的元素:
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.");
}
}
}
}
类型转换
根据预定义的转换规则、继承结构和显式转换定义,给定类型的值可能可以显式或隐式转换为其他类型。
预定义转换
许多预定义的值类型支持与其他预定义值类型之间的转换。如果类型转换能够保证不丢失信息,则可以进行隐式转换(即无需显式类型转换)。
继承多态性
一个值可以隐式转换为任何它继承的类或实现的接口类型。要将基类转换为继承它的类,或将接口实例转换为实现它的类,必须进行显式转换以使转换语句能够编译。在这两种情况下,如果待转换的值不是目标类型或其派生类型的实例,运行时环境会抛出转换异常。
作用域与生命周期
变量的作用域和生命周期取决于其声明位置。
- 作用域:参数和局部变量的作用域对应于声明它们的方法或语句块,而字段的作用域与实例或类相关,并可能进一步受到访问修饰符的限制。
- 生命周期:变量的生命周期由运行时环境决定,使用隐式引用计数和复杂的垃圾回收算法进行管理。