第 06 课:变量与数据类型
更新: 6/16/2026 字数: 0 字 时长: 0 分钟
为什么学这个
上一课你写了第一个 C# 程序,在控制台打印了一行 "Hello World"。那行代码里其实已经用到了变量和数据类型,只是当时没展开说。
变量是程序里最基础的东西。没有变量,程序连一个数字都记不住。数据类型告诉编译器"这个变量里装的是什么",编译器才好分配内存、检查你有没有写错。不弄清楚这两样,后面的循环、方法、面向对象全都没法写。
这一课用 TubaTools 项目里真实出现的变量和数据类型来举例。你看到的每一行代码,都在那个工具集项目里实际跑着。
变量:一个带名字的盒子
变量就是内存里一块位置,你给它起个名字,往里面放数据,以后用这个名字就能把数据取出来。
打个比方:你去超市存包,柜子给你一个小票,上面印着"13 号柜"。变量名就是那个编号,内存就是柜子,数据就是包。以后你报"13 号柜",就能把包拿回来。
在 C# 里声明一个变量,写法是:
数据类型 变量名 = 初始值;比如:
int age = 25;
string name = "张三";
bool isStudent = true;左边 int、string、bool 是数据类型,右边是值。等号在这里叫"赋值",意思是把右边的值装进左边的变量里。
命名规则
变量名不是随便起的,有几条硬规定:
- 只能包含字母、数字、下划线
- 不能以数字开头
- 不能是 C# 关键字(比如
int、class、public这些已经被语言占用了) - C# 区分大小写,
age和Age是两个不同的变量
除了硬规定,还有一些约定俗成的写法。C# 社区普遍用驼峰命名法(camelCase)给变量起名:第一个单词小写,后面每个单词首字母大写,比如 isStudent、toolName、wingetInstallProgress。
对私有字段(private field),很多人喜欢加下划线前缀,比如 _iconPath、_isFavorite。你在 TubaTools 源码里会看到大量这种写法。
C# 的常用数据类型
C# 的数据类型分两大类:值类型和引用类型。这个区别很重要,但先不展开,后面讲面向对象时再说。现在先认识最常用的几种。
int:整数
int 存整数,范围大约正负 21 亿。够绝大多数场景用了。
int count = 0;
int maxUsers = 10000;
int year = 2025;TubaTools 里有一个很典型的例子。ToolItem.cs 中有一个字段记录 winget 安装进度:
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。
double price = 19.99;
double temperature = 36.5;
float rating = 4.5f; // 注意 f 后缀,不加 f 编译器会报错string:字符串
string 存文本。它大概是程序里出现频率最高的类型。用来存名字、路径、描述、错误信息,什么都行。
string name = "TubaTools";
string path = @"C:\Tools\cpu-z.exe";
string empty = ""; // 空字符串
string nothing = null; // null,字符串变量可以不指向任何东西声明字符串用双引号。注意路径里如果有反斜杠 \,要么写成 \\,要么在字符串前面加 @,让编译器把反斜杠当普通字符处理。
TubaTools 里几乎每个属性都是 string。看 ToolItem.cs:
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 只有两个值:true 和 false。用来做开关、记状态、做判断条件。
bool isRunning = true;
bool hasError = false;在 TubaTools 里,bool 大量出现。ToolItem.cs 里判断一个工具要不要下载,靠的就是 bool 属性:
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 标记工具是否被收藏。这两个属性的值只能在 true 和 false 之间切换。
char:单个字符
char 存一个字符,用单引号:
char grade = 'A';
char separator = ',';平时不太单独用,更多是配合字符串处理。知道有这个东西就行。
可空类型:值后面那个问号
看 TubaTools 的代码,你会发现很多 string?:
public string? IconPath { get; set; }
public string? Description { get; init; }
public string? Publisher { get; init; }问号表示这个变量可以是 null。null 的意思是"没有值""空的引用"。
为什么需要这个?因为不是每个工具都有图标路径、都有描述。对于那些没有的工具,这些属性就是 null,而不是空字符串 ""。UI 层会根据 null 来决定是否显示对应元素。
对于值类型,比如 int,也可以加问号变成可空:
int? score = null; // 合法
int? score2 = 95; // 也合法TubaTools 里 PrimaryArch 就是可空的:
public string? PrimaryArch { get; init; }一个工具可能没有主架构版本(比如纯脚本),这时候 PrimaryArch 是 null,等待后面的逻辑去处理。
var:让编译器猜类型
C# 有个关键字 var,写起来很省事:
var name = "TubaTools"; // 编译器推断为 string
var count = 42; // 编译器推断为 int
var price = 19.99; // 编译器推断为 double
var isReady = true; // 编译器推断为 boolvar 不是动态类型,C# 仍然是强类型语言。编译器在编译时根据右边的值推断出确切类型,你写 var name = "hello" 和写 string name = "hello" 编译出来的结果完全一样。
什么时候用 var?右边类型已经很明确了(比如 new List<string>()),用 var 少打字。右边不太明确(比如一个方法调用的返回值),用显式类型更清晰。
TubaTools 源码里 var 出现频率很高。看 InitArchOptions 方法:
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。
变量的作用域
变量不是全局有效的。在哪个大括号 {} 里声明,就在哪个大括号里有效,出了大括号就消失了。这叫"作用域"。
{
int a = 10;
Console.WriteLine(a); // 没问题
}
// Console.WriteLine(a); // 编译错误:a 不存在这个规则导致了一个常见的需求:字段。在类的顶层声明、不在任何方法内的变量,叫字段。字段在整个类的范围内都有效。TubaTools 里所有 private 开头的变量都是字段:
private string? _iconPath;
private bool _isFavorite;
private int _wingetInstallProgress;
private bool _isWingetInstalled;这些字段在 ToolItem 类的任何一个方法里都能读写。
类型转换
有时候你需要把一种类型变成另一种。
隐式转换:自动发生的
小范围类型可以自动变成大范围类型,不会丢数据:
int a = 100;
long b = a; // int 自动转 long,没问题
float c = 3.14f;
double d = c; // float 自动转 double显式转换:手动告诉编译器
大转小可能丢数据,编译器要求你明确写出转换,表示你清楚风险:
double pi = 3.14159;
int approxPi = (int)pi; // 结果是 3,小数部分丢掉了字符串转换:Parse 和 ToString
把字符串变成数字:
string input = "42";
int number = int.Parse(input); // 变成整数 42
double d = double.Parse("3.14"); // 变成浮点数 3.14把其他类型变成字符串用 ToString():
int x = 100;
string s = x.ToString(); // "100"TubaTools 里 ArchOption 类就重写了 ToString:
public override string ToString() => DisplayText;这样当 UI 框架需要把 ArchOption 对象展示成文字时,自动调用 ToString() 拿到可读的文本。
下面这张图帮你梳理
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# 常用的数据类型基本齐了:
| 字段 | 数据类型 | 说明 |
|---|---|---|
Name | string | 工具名称,必填 |
Category | string | 工具分类,必填 |
Path | string | 工具完整路径,必填 |
_iconPath | string? | 图标路径,可空 |
Description | string? | 描述文字,可空 |
Version | string? | 版本号,可空 |
_isFavorite | bool | 是否收藏,非空,默认 false |
_wingetInstallProgress | int | 安装进度,非空 |
_wingetInstallStatus | string | 安装状态文字,非空但初始化为空串 |
NeedsDownload | bool | 计算属性,判断是否需要下载 |
Tags | IReadOnlyList<string> | 标签列表,引用类型 |
ArchOptions | ObservableCollection<ArchOption> | 架构选项集合 |
这里有个有意思的细节:_wingetInstallStatus 的类型是 string 不是 string?。但它初始化为空字符串 ""。这揭示了设计意图:安装状态即使没有实际内容,也表示"当前状态为空",而不是"状态不存在"。string 和 string? 的选择不是随意的,它表达了"这个值是否允许缺失"的语义。
再看 Folder 属性:
public string Folder => System.IO.Path.GetDirectoryName(RelativePath) ?? Category;这里使用了 ?? 运算符,意思是"如果左边是 null,就用右边的值"。GetDirectoryName 可能返回 null,这时用 Category 兜底。这种写法在 TubaTools 里到处都是。
小练习
第 1 题(填空):下面的变量声明中,填上正确的数据类型。
______ 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 题(简答):string 和 string? 的区别是什么?在 TubaTools 的 ToolItem.cs 里,哪些属性用了 string,哪些用了 string??这样设计的理由是什么?(翻回上面看看代码再回答。)
第 4 题(实操):打开 TubaTools 源码,找到 Models/ToolItem.cs,数一数这个文件里出现了多少种不同的数据类型(string、bool、int、IReadOnlyList<string> 等)。把它们列出来,旁边标注这个类型在 ToolItem 中用来存什么信息。
第 5 题(实操):新建一个控制台项目,声明一个 int 变量存你的年龄,一个 string 变量存你的名字,一个 bool 变量标记你是否学过编程,然后打印出来。最后把这三样组合成一句话输出,比如"张三今年 25 岁,学过编程:False"。
参考答案(做完再点开)
第 1 题:
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.cs 里 Name、Category、Path、RelativePath、Extension 用了 string(必填,不能为 null)。IconPath、Description、Publisher、Version、DatabaseSource、DownloadUrl 等用了 string?,因为这些信息不是每个工具都有。设计理由:把必填和非必填在类型层面就区分开,编译器能帮你检查,防止空引用异常。
第 4 题:ToolItem.cs 中出现的数据类型:string(名称、路径等)、bool(收藏状态、下载状态)、int(安装进度)、IReadOnlyList<string>(标签列表)、ObservableCollection<ArchOption>(架构选项集合)、ArchOption?(选中的架构,可空)。ArchVariant 和 ArchOption 类本身也包含了 string 类型的属性。
第 5 题:
int age = 25;
string name = "张三";
bool hasLearned = false;
Console.WriteLine($"{name}今年{age}岁,学过编程:{hasLearned}");