Skip to content

第 06 课:变量与数据类型

更新: 6/16/2026 字数: 0 字 时长: 0 分钟

为什么学这个

上一课你写了第一个 C# 程序,在控制台打印了一行 "Hello World"。那行代码里其实已经用到了变量和数据类型,只是当时没展开说。

变量是程序里最基础的东西。没有变量,程序连一个数字都记不住。数据类型告诉编译器"这个变量里装的是什么",编译器才好分配内存、检查你有没有写错。不弄清楚这两样,后面的循环、方法、面向对象全都没法写。

这一课用 TubaTools 项目里真实出现的变量和数据类型来举例。你看到的每一行代码,都在那个工具集项目里实际跑着。

变量:一个带名字的盒子

变量就是内存里一块位置,你给它起个名字,往里面放数据,以后用这个名字就能把数据取出来。

打个比方:你去超市存包,柜子给你一个小票,上面印着"13 号柜"。变量名就是那个编号,内存就是柜子,数据就是包。以后你报"13 号柜",就能把包拿回来。

在 C# 里声明一个变量,写法是:

csharp
数据类型 变量名 = 初始值;

比如:

csharp
int age = 25;
string name = "张三";
bool isStudent = true;

左边 intstringbool 是数据类型,右边是值。等号在这里叫"赋值",意思是把右边的值装进左边的变量里。

命名规则

变量名不是随便起的,有几条硬规定:

  1. 只能包含字母、数字、下划线
  2. 不能以数字开头
  3. 不能是 C# 关键字(比如 intclasspublic 这些已经被语言占用了)
  4. C# 区分大小写,ageAge 是两个不同的变量

除了硬规定,还有一些约定俗成的写法。C# 社区普遍用驼峰命名法(camelCase)给变量起名:第一个单词小写,后面每个单词首字母大写,比如 isStudenttoolNamewingetInstallProgress

对私有字段(private field),很多人喜欢加下划线前缀,比如 _iconPath_isFavorite。你在 TubaTools 源码里会看到大量这种写法。

C# 的常用数据类型

C# 的数据类型分两大类:值类型和引用类型。这个区别很重要,但先不展开,后面讲面向对象时再说。现在先认识最常用的几种。

int:整数

int 存整数,范围大约正负 21 亿。够绝大多数场景用了。

csharp
int count = 0;
int maxUsers = 10000;
int year = 2025;

TubaTools 里有一个很典型的例子。ToolItem.cs 中有一个字段记录 winget 安装进度:

csharp
private int _wingetInstallProgress;
public int WingetInstallProgress
{
    get => _wingetInstallProgress;
    set => SetField(ref _wingetInstallProgress, value);
}

你下载工具时,进度条从 0 走到 100,靠的就是这个 int 变量在背后记着当前百分比。数字能比较(到了 100 没有?),能做加法(加一个增量),这些都是 int 的能力。

double 和 float:小数

double 存双精度浮点数,float 存单精度。绝大多数时候用 double 就够了。写小数时,C# 默认把字面量当成 double

csharp
double price = 19.99;
double temperature = 36.5;
float rating = 4.5f;  // 注意 f 后缀,不加 f 编译器会报错

string:字符串

string 存文本。它大概是程序里出现频率最高的类型。用来存名字、路径、描述、错误信息,什么都行。

csharp
string name = "TubaTools";
string path = @"C:\Tools\cpu-z.exe";
string empty = "";  // 空字符串
string nothing = null;  // null,字符串变量可以不指向任何东西

声明字符串用双引号。注意路径里如果有反斜杠 \,要么写成 \\,要么在字符串前面加 @,让编译器把反斜杠当普通字符处理。

TubaTools 里几乎每个属性都是 string。看 ToolItem.cs

csharp
public required string Name { get; init; }
public required string Category { get; init; }
public required string Path { get; init; }
public required string RelativePath { get; init; }
public required string Extension { get; init; }
public string? Description { get; init; }
public string? Publisher { get; init; }
public string? Version { get; init; }

一个工具项的"名称""分类""路径""描述""发行商""版本号"全是 string。注意有些带了问号 string?,这个下面说。

bool:布尔值

bool 只有两个值:truefalse。用来做开关、记状态、做判断条件。

csharp
bool isRunning = true;
bool hasError = false;

在 TubaTools 里,bool 大量出现。ToolItem.cs 里判断一个工具要不要下载,靠的就是 bool 属性:

csharp
public bool NeedsDownload => !string.IsNullOrWhiteSpace(DownloadUrl) 
    || !string.IsNullOrWhiteSpace(WingetId) 
    || !string.IsNullOrWhiteSpace(RemoteUrl);

private bool _isFavorite;
public bool IsFavorite
{
    get => _isFavorite;
    set => SetField(ref _isFavorite, value);
}

NeedsDownload 的值取决于三个条件中是否至少有一个成立。IsFavorite 标记工具是否被收藏。这两个属性的值只能在 truefalse 之间切换。

char:单个字符

char 存一个字符,用单引号:

csharp
char grade = 'A';
char separator = ',';

平时不太单独用,更多是配合字符串处理。知道有这个东西就行。

可空类型:值后面那个问号

看 TubaTools 的代码,你会发现很多 string?

csharp
public string? IconPath { get; set; }
public string? Description { get; init; }
public string? Publisher { get; init; }

问号表示这个变量可以是 nullnull 的意思是"没有值""空的引用"。

为什么需要这个?因为不是每个工具都有图标路径、都有描述。对于那些没有的工具,这些属性就是 null,而不是空字符串 ""。UI 层会根据 null 来决定是否显示对应元素。

对于值类型,比如 int,也可以加问号变成可空:

csharp
int? score = null;  // 合法
int? score2 = 95;   // 也合法

TubaTools 里 PrimaryArch 就是可空的:

csharp
public string? PrimaryArch { get; init; }

一个工具可能没有主架构版本(比如纯脚本),这时候 PrimaryArchnull,等待后面的逻辑去处理。

var:让编译器猜类型

C# 有个关键字 var,写起来很省事:

csharp
var name = "TubaTools";   // 编译器推断为 string
var count = 42;            // 编译器推断为 int
var price = 19.99;         // 编译器推断为 double
var isReady = true;        // 编译器推断为 bool

var 不是动态类型,C# 仍然是强类型语言。编译器在编译时根据右边的值推断出确切类型,你写 var name = "hello" 和写 string name = "hello" 编译出来的结果完全一样。

什么时候用 var?右边类型已经很明确了(比如 new List<string>()),用 var 少打字。右边不太明确(比如一个方法调用的返回值),用显式类型更清晰。

TubaTools 源码里 var 出现频率很高。看 InitArchOptions 方法:

csharp
var primary = new ArchOption { Name = Name, Path = Path, Arch = PrimaryArch ?? "" };
var isArm64 = RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
var isX64 = Environment.Is64BitOperatingSystem && !isArm64;
var preferred = ArchOptions.FirstOrDefault(a => ...) ?? primary;

这些地方用显式类型也不会错,但 var 让代码更干净。关键是变量名要起得好,isArm64 一看就知道是 bool

变量的作用域

变量不是全局有效的。在哪个大括号 {} 里声明,就在哪个大括号里有效,出了大括号就消失了。这叫"作用域"。

csharp
{
    int a = 10;
    Console.WriteLine(a);  // 没问题
}
// Console.WriteLine(a);  // 编译错误:a 不存在

这个规则导致了一个常见的需求:字段。在类的顶层声明、不在任何方法内的变量,叫字段。字段在整个类的范围内都有效。TubaTools 里所有 private 开头的变量都是字段:

csharp
private string? _iconPath;
private bool _isFavorite;
private int _wingetInstallProgress;
private bool _isWingetInstalled;

这些字段在 ToolItem 类的任何一个方法里都能读写。

类型转换

有时候你需要把一种类型变成另一种。

隐式转换:自动发生的

小范围类型可以自动变成大范围类型,不会丢数据:

csharp
int a = 100;
long b = a;  // int 自动转 long,没问题
float c = 3.14f;
double d = c;  // float 自动转 double

显式转换:手动告诉编译器

大转小可能丢数据,编译器要求你明确写出转换,表示你清楚风险:

csharp
double pi = 3.14159;
int approxPi = (int)pi;  // 结果是 3,小数部分丢掉了

字符串转换:Parse 和 ToString

把字符串变成数字:

csharp
string input = "42";
int number = int.Parse(input);      // 变成整数 42
double d = double.Parse("3.14");    // 变成浮点数 3.14

把其他类型变成字符串用 ToString()

csharp
int x = 100;
string s = x.ToString();  // "100"

TubaTools 里 ArchOption 类就重写了 ToString

csharp
public override string ToString() => DisplayText;

这样当 UI 框架需要把 ArchOption 对象展示成文字时,自动调用 ToString() 拿到可读的文本。

下面这张图帮你梳理

mermaid
graph TD
    A[声明变量] --> B{值类型 or 引用类型?}
    B -->|值类型| C[int / double / bool / char]
    B -->|引用类型| D[string / 类对象 / 数组]
    C --> E[存的是值本身]
    D --> F[存的是引用地址]
    
    A --> G[显式类型]
    A --> H[var 推断]
    
    A --> I[可空吗?]
    I -->|是| J[类型名后加 ?<br/>string? / int?]
    I -->|否| K[不能赋 null]
    
    J --> L[null 表示"没有值"]
    
    C --> M[声明示例<br/>int count = 0;]
    D --> N[声明示例<br/>string name = TubaTools;]
    J --> O[声明示例<br/>string? desc = null;]

值类型和引用类型的区别一句话说:值类型里存的是数据本身,引用类型里存的是数据在内存里的地址。把一个 int 赋给另一个 int,复制的是数值。把一个对象赋给另一个变量,复制的是地址,两个变量指向同一个对象。这个在后面讲类和对象时会展开。

回到 TubaTools:ToolItem 里的全部数据类型

ToolItem.cs 里的所有字段一列,C# 常用的数据类型基本齐了:

字段数据类型说明
Namestring工具名称,必填
Categorystring工具分类,必填
Pathstring工具完整路径,必填
_iconPathstring?图标路径,可空
Descriptionstring?描述文字,可空
Versionstring?版本号,可空
_isFavoritebool是否收藏,非空,默认 false
_wingetInstallProgressint安装进度,非空
_wingetInstallStatusstring安装状态文字,非空但初始化为空串
NeedsDownloadbool计算属性,判断是否需要下载
TagsIReadOnlyList<string>标签列表,引用类型
ArchOptionsObservableCollection<ArchOption>架构选项集合

这里有个有意思的细节:_wingetInstallStatus 的类型是 string 不是 string?。但它初始化为空字符串 ""。这揭示了设计意图:安装状态即使没有实际内容,也表示"当前状态为空",而不是"状态不存在"。stringstring? 的选择不是随意的,它表达了"这个值是否允许缺失"的语义。

再看 Folder 属性:

csharp
public string Folder => System.IO.Path.GetDirectoryName(RelativePath) ?? Category;

这里使用了 ?? 运算符,意思是"如果左边是 null,就用右边的值"。GetDirectoryName 可能返回 null,这时用 Category 兜底。这种写法在 TubaTools 里到处都是。

小练习

第 1 题(填空):下面的变量声明中,填上正确的数据类型。

csharp
______ toolCount = 128;
______ isDownloaded = false;
______ toolName = "CPU-Z";
______ versionNumber = 2.09;
______ description = null;

第 2 题(选择):下面哪行代码会编译报错?

A. int x = 10; B. string s = "hello"; C. int y = "20"; D. var z = 3.14;

第 3 题(简答)stringstring? 的区别是什么?在 TubaTools 的 ToolItem.cs 里,哪些属性用了 string,哪些用了 string??这样设计的理由是什么?(翻回上面看看代码再回答。)

第 4 题(实操):打开 TubaTools 源码,找到 Models/ToolItem.cs,数一数这个文件里出现了多少种不同的数据类型(stringboolintIReadOnlyList<string> 等)。把它们列出来,旁边标注这个类型在 ToolItem 中用来存什么信息。

第 5 题(实操):新建一个控制台项目,声明一个 int 变量存你的年龄,一个 string 变量存你的名字,一个 bool 变量标记你是否学过编程,然后打印出来。最后把这三样组合成一句话输出,比如"张三今年 25 岁,学过编程:False"。


参考答案(做完再点开)

第 1 题

csharp
int toolCount = 128;
bool isDownloaded = false;
string toolName = "CPU-Z";
double versionNumber = 2.09;
string? description = null;

第 2 题:C。"20" 是字符串,不能直接赋给 int 变量。需要 int.Parse("20") 转换。

第 3 题string 表示这个变量不能为 null,string? 允许为 null。ToolItem.csNameCategoryPathRelativePathExtension 用了 string(必填,不能为 null)。IconPathDescriptionPublisherVersionDatabaseSourceDownloadUrl 等用了 string?,因为这些信息不是每个工具都有。设计理由:把必填和非必填在类型层面就区分开,编译器能帮你检查,防止空引用异常。

第 4 题ToolItem.cs 中出现的数据类型:string(名称、路径等)、bool(收藏状态、下载状态)、int(安装进度)、IReadOnlyList<string>(标签列表)、ObservableCollection<ArchOption>(架构选项集合)、ArchOption?(选中的架构,可空)。ArchVariantArchOption 类本身也包含了 string 类型的属性。

第 5 题

csharp
int age = 25;
string name = "张三";
bool hasLearned = false;
Console.WriteLine($"{name}今年{age}岁,学过编程:{hasLearned}");
 ·