Day 1 - 类与对象
建议用时:180-210 分钟
你将学会什么
- 为什么程序需要类和对象
class Product、new Product()、变量product分别是什么- 属性、方法、构造函数分别负责什么
- 对象初始化器和构造函数有什么区别
- 为什么要把数据和行为放到同一个类里
- 如何写一个有校验、有方法的商品类
第三周开始进入面向对象。不要把类理解成语法壳子,先把它理解成“把一类数据和它的动作放在一起”。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“复盘与巩固”,今天继续学习“类与对象”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“类与对象”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。
今天先抓住什么
前面写程序时,数据和方法经常是分散的:
string name
decimal price
int count
CalculateTotal(price, count)
PrintProduct(name, price)类的作用是把一类数据和相关动作放到一起:
Product
Name
Price
ChangePrice
CalculateTotal
Print这样代码更像业务本身。
名词先讲清楚
| 名词 | 直接解释 | 例子 |
|---|---|---|
| 类 | 一类对象的模板 | class Product |
| 对象 | 按类创建出来的具体实例 | new Product(...) |
| 实例 | 对象的另一种说法 | 一个具体商品 |
| 属性 | 对象保存的数据 | Name、Price |
| 方法 | 对象能执行的动作 | ChangePrice |
| 构造函数 | 创建对象时执行的初始化方法 | public Product(...) |
new | 创建对象 | new Product(...) |
this | 当前对象自己 | this.Name |
| 封装 | 把数据和规则放在一起 | 价格校验放进 Product |
从今天开始按文件顺序学习
Day 5 学的是 Program.cs 里的本地方法,所以可以在同一个文件中先定义、后调用。
从今天开始学习类,真实项目通常把类和使用它的代码放在不同文件:
第 1 步:先写 Product.cs,定义 Product 类、属性和方法
第 2 步:再写 Program.cs,创建 Product 对象并调用方法后面的示例如果标了文件名,就按文件名逐个创建,不要把两个文件的代码全塞进同一个 Program.cs。
有些完整示例为了方便展示,会把顶级语句和类放在同一个代码块里。那时 C# 的顶级语句必须写在类声明前面,但并不是“运行到后面才创建这个类”。编译时,C# 会先检查并生成整个项目里的类和方法;运行时,才从程序入口开始执行。练习时优先按 Product.cs、Program.cs 两个文件来写,最不容易混乱。
类和对象的区别
类是模板,对象是具体数据。
先创建 Product.cs,定义类:
class Product
{
public string Name { get; }
public decimal Price { get; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
public void Print()
{
Console.WriteLine($"{Name}: {Price:F2}");
}
}再打开 Program.cs,创建和使用对象:
var keyboard = new Product("Keyboard", 199m);
var mouse = new Product("Mouse", 59m);
keyboard.Print();
mouse.Print();Product 是类,keyboard 和 mouse 是两个不同对象。它们使用同一张模板,但保存不同数据。
属性保存状态
属性回答“这个对象现在是什么样”。继续使用刚才的 Product.cs,这里只替换 Program.cs:
var product = new Product("Keyboard", 199m);
Console.WriteLine(product.Name);
Console.WriteLine(product.Price);输出:
Keyboard
199方法表达动作
方法回答“这个对象能做什么”。先在 Product.cs 的类里面加入方法定义:
public decimal CalculateTotal(int count)
{
return Price * count;
}再在 Program.cs 调用已经定义好的对象方法:
var product = new Product("Keyboard", 199m);
decimal total = product.CalculateTotal(3);
Console.WriteLine(total);CalculateTotal 放在 Product 里,是因为总价计算和商品价格密切相关。
构造函数保证对象一开始就有效
如果这样写:
var product = new Product();
product.Name = "";
product.Price = -1m;对象可能一开始就是错误状态。更稳的做法是在构造函数里收齐必要数据并校验。
先把带校验的构造函数写进 Product.cs:
class Product
{
public string Name { get; }
public decimal Price { get; }
public Product(string name, decimal price)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名称不能为空");
}
if (price < 0)
{
throw new ArgumentException("价格不能小于 0");
}
Name = name.Trim();
Price = price;
}
}再在 Program.cs 创建对象:
var product = new Product("Keyboard", 199m);
Console.WriteLine(product.Name);get、set、private set
public string Name { get; }
public decimal Price { get; private set; }意思是:
| 写法 | 含义 |
|---|---|
{ get; } | 外部只能读取,不能修改 |
{ get; set; } | 外部可以读取,也可以修改 |
{ get; private set; } | 外部可以读取,类内部可以修改 |
价格一般不建议外部随便改,可以通过 ChangePrice 方法统一校验。
对象初始化器
对象初始化器适合简单数据类:
先创建 Customer.cs:
class Customer
{
public string Name { get; set; } = "";
public string Level { get; set; } = "";
}再在 Program.cs 创建对象:
var customer = new Customer
{
Name = "Tom",
Level = "VIP"
};
Console.WriteLine($"{customer.Name}: {customer.Level}");如果创建对象时必须校验必要数据,优先用构造函数。
类和对象常用写法速查
| 需求 | 写法 | 说明 |
|---|---|---|
| 定义类 | class Product { } | 描述一种对象 |
| 创建对象 | new Product(...) | 得到一个具体对象 |
| 只读属性 | public string Name { get; } | 构造后不允许改 |
| 内部可改属性 | public decimal Price { get; private set; } | 外部不能直接改 |
| 构造函数 | public Product(...) | 保证对象创建时有效 |
| 对象方法 | product.ChangePrice(99m) | 让对象自己处理规则 |
| 对象初始化器 | new Product { Name = "A" } | 适合简单数据对象 |
| 放进列表 | List<Product> | 保存多个对象 |
| 遍历对象 | foreach (Product p in products) | 批量处理对象 |
常用模板:
class Product
{
public string Name { get; }
public decimal Price { get; private set; }
public Product(string name, decimal price)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("商品名称不能为空");
}
if (price <= 0)
{
throw new ArgumentException("价格必须大于 0");
}
Name = name.Trim();
Price = price;
}
public void ChangePrice(decimal price)
{
if (price <= 0)
{
throw new ArgumentException("价格必须大于 0");
}
Price = price;
}
}这就是类最常见的写法:属性保存状态,构造函数保证初始有效,方法负责修改规则。
今天写类时按这个顺序
- 先找业务名词,例如商品、客户、订单。
- 把名词变成类。
- 把这类数据变成属性。
- 把这类动作变成方法。
- 用构造函数保证必要数据不为空。
- 创建对象并调用方法验证。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个 Product 类,用它创建商品对象,并让对象自己计算总价。
最终你要写出这个程序
var product = new Product("Keyboard", 199m);
product.ChangePrice(179m);
product.Print();
decimal total = product.CalculateTotal(2);
Console.WriteLine($"购买 2 个总价: {total:F2}");
class Product
{
public string Name { get; }
public decimal Price { get; private set; }
public Product(string name, decimal price)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("商品名称不能为空");
}
if (price < 0)
{
throw new ArgumentException("价格不能小于 0");
}
Name = name.Trim();
Price = price;
}
public void ChangePrice(decimal price)
{
if (price < 0)
{
throw new ArgumentException("价格不能小于 0");
}
Price = price;
}
public decimal CalculateTotal(int count)
{
if (count <= 0)
{
throw new ArgumentException("数量必须大于 0");
}
return Price * count;
}
public void Print()
{
Console.WriteLine($"{Name}: {Price:F2}");
}
}输出:
Keyboard: 179.00
购买 2 个总价: 358.00这个例子里,Product 类把商品名称、价格、改价、计算总价、打印信息放在了一起。
第三部分:跟着敲代码
从这里开始动手。每个例子都可以直接放进 Program.cs 运行。
例子 1:创建第一个类和对象
var product = new Product();
product.Name = "Keyboard";
product.Price = 199m;
Console.WriteLine($"{product.Name}: {product.Price:F2}");
class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}预期输出:
Keyboard: 199.00例子 2:给类添加方法
var product = new Product();
product.Name = "Keyboard";
product.Price = 199m;
decimal total = product.CalculateTotal(2);
Console.WriteLine($"总价: {total:F2}");
class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public decimal CalculateTotal(int count)
{
return Price * count;
}
}例子 3:用构造函数创建有效对象
var product = new Product("Keyboard", 199m);
product.Print();
class Product
{
public string Name { get; }
public decimal Price { get; }
public Product(string name, decimal price)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名称不能为空");
}
if (price < 0)
{
throw new ArgumentException("价格不能小于 0");
}
Name = name.Trim();
Price = price;
}
public void Print()
{
Console.WriteLine($"{Name}: {Price:F2}");
}
}例子 4:用方法统一修改价格
var product = new Product("Keyboard", 199m);
product.ChangePrice(179m);
product.Print();
class Product
{
public string Name { get; }
public decimal Price { get; private set; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
public void ChangePrice(decimal price)
{
if (price < 0)
{
throw new ArgumentException("价格不能小于 0");
}
Price = price;
}
public void Print()
{
Console.WriteLine($"{Name}: {Price:F2}");
}
}例子 5:多个对象放进 List
var products = new List<Product>
{
new("Keyboard", 199m),
new("Mouse", 59m),
new("Cable", 19m)
};
foreach (Product product in products)
{
product.Print();
}
class Product
{
public string Name { get; }
public decimal Price { get; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
public void Print()
{
Console.WriteLine($"{Name}: {Price:F2}");
}
}例子 6:对象初始化器
var customer = new Customer
{
Name = "Tom",
Level = "VIP"
};
Console.WriteLine($"{customer.Name}: {customer.Level}");
class Customer
{
public string Name { get; set; } = "";
public string Level { get; set; } = "";
}常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 类里只放字段不放行为 | 对象只变成数据袋 | 把和数据强相关的规则放进方法 |
| 属性没有默认值 | 创建对象后可能出现空值 | 给字符串属性设置 = "" 或通过构造函数传入 |
| 构造函数参数太乱 | 创建对象时容易传错 | 参数顺序按字段重要性排列,必要时分行写 |
| 方法直接修改外部集合 | 对象边界不清楚 | 让对象只管理自己的状态 |
所有类都写在 Program.cs | 项目变大后难维护 | 小练习可放一起,项目里按类拆文件 |
小白重复敲写训练
类和对象要靠反复创建不同对象来记忆。
训练 1:第一个商品类
var product = new Product();
product.Name = "Keyboard";
product.Price = 199m;
Console.WriteLine($"{product.Name}: {product.Price:F2}");
class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}第二遍再创建一个 Mouse 对象,不要修改第一个对象。
训练 2:给类增加方法
var product = new Product { Name = "Mouse", Price = 59m };
product.Print();
class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public void Print()
{
Console.WriteLine($"{Name}: {Price:F2}");
}
}改动任务:增加 Stock 属性,并在 Print 中输出库存。
训练 3:构造时传入数据
var product = new Product("Monitor", 899m);
Console.WriteLine(product.Name);
class Product
{
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
public string Name { get; }
public decimal Price { get; }
}第三遍关掉答案,自己写出构造方法。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:类与对象。今天至少要掌握的 3 个点是:
1. 为什么程序需要类和对象
2. `class Product`、`new Product()`、变量 `product` 分别是什么
3. 属性、方法、构造函数分别负责什么答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:为什么程序需要类和对象。
上位机专项练习
上位机中的设备、点位、采集记录和报警都适合建模成类。先定义类型,再创建对象。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:定义并创建设备对象
class Device
{
public string Name { get; }
public bool IsOnline { get; private set; }
public Device(string name) { Name = name; }
public void Connect() { IsOnline = true; }
}
class Program
{
static void Main()
{
var device = new Device("PLC-01");
device.Connect();
Console.WriteLine($"{device.Name}: {device.IsOnline}");
}
}运行结果或界面效果:
PLC-01: True改动任务: 增加 Disconnect 方法,把 IsOnline 改回 false。
专项例子 2:用构造函数校验设备编号
class Device
{
public string Id { get; }
public Device(string id)
{
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("设备编号不能为空");
Id = id.Trim();
}
}
class Program
{
static void Main()
{
var device = new Device(" PLC-02 ");
Console.WriteLine(device.Id);
}
}运行结果或界面效果:
PLC-02改动任务: 传入空字符串,观察异常。
专项例子 3:让设备自己判断报警
class TemperatureDevice
{
public double AlarmLimit { get; }
public TemperatureDevice(double alarmLimit) { AlarmLimit = alarmLimit; }
public bool IsAlarm(double value) { return value >= AlarmLimit; }
}
class Program
{
static void Main()
{
var device = new TemperatureDevice(80);
Console.WriteLine(device.IsAlarm(82.5));
}
}运行结果或界面效果:
True改动任务: 把报警线改成 90。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个 Product 类,要求:
- 有
Name属性。 - 有
Price属性,外部只能读取,类内部可以修改。 - 构造函数里校验名称不能为空、价格不能小于 0。
ChangePrice方法可以修改价格,但新价格不能小于 0。CalculateTotal方法根据数量计算总价。Print方法输出商品信息。- 创建多个商品,放进
List<Product>,逐个输出。
完整代码
var products = new List<Product>
{
new("Keyboard", 199m),
new("Mouse", 59m),
new("Cable", 19m)
};
products[0].ChangePrice(179m);
foreach (Product product in products)
{
product.Print();
Console.WriteLine($"购买 2 个总价: {product.CalculateTotal(2):F2}");
}
class Product
{
public string Name { get; }
public decimal Price { get; private set; }
public Product(string name, decimal price)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("商品名称不能为空");
}
if (price < 0)
{
throw new ArgumentException("价格不能小于 0");
}
Name = name.Trim();
Price = price;
}
public void ChangePrice(decimal price)
{
if (price < 0)
{
throw new ArgumentException("价格不能小于 0");
}
Price = price;
}
public decimal CalculateTotal(int count)
{
if (count <= 0)
{
throw new ArgumentException("数量必须大于 0");
}
return Price * count;
}
public void Print()
{
Console.WriteLine($"{Name}: {Price:F2}");
}
}正确运行结果
你应该能看到:
Keyboard: 179.00
购买 2 个总价: 358.00
Mouse: 59.00
购买 2 个总价: 118.00
Cable: 19.00
购买 2 个总价: 38.00你要能说清楚这 6 件事
- 类是模板,对象是具体实例。
- 属性保存对象状态。
- 方法表达对象动作。
- 构造函数负责创建对象时初始化数据。
private set可以防止外部绕过校验随便改值。- 对象可以放进
List,再逐个调用方法。