C#学习记录

### 2 C#编程概述
###
using System;//告诉编译器这个程序使用System命名空间的类型
namespace Simple//声明一个新命名空间,名称为Simple
{
calss Program//声明一个新的类类型,名称为Program
{
static void Main()//声明一个名称为Main的方法作为类Program的成员
{
Console.WriteLine("Hi there!");
}
}
}
2.2标识符

2.7 格式化数字字符串

格式化
Console.WriteLine("The value:{0},",500);
Console.WriteLine("The value:{0:C},",500);
3.5 预定义类型

### 4 类的基本概念
class DaysTemp
{
public int High,low;
public int Average()
{
return (High + low) / 2;
}
}
class Program{
{
static void Main()
{
DaysTemp t1 = new DaysTemp();
DaysTemp t2 = new DaysTemp();
t1.High = 76; t1.low = 57;
t2.High = 75; t2.low = 52;
Console.WriteLine("t1:{0},{1},{2}",
t1.High,t1.low,t1.Average());
Console.WriteLine("t2:{0},{1},{2}",
t2.High,t2.low,t2.Average());
}
}
}
### 5 方法
方法体

方法体的组成

##### 5.3 本地变量
作用:本地变量也保存数据 和 字段一样

特点:
1.声明后存在→完成执行后结束
2.可以在方法体的任一位置声明,但必须在使用前声明
下面是两个本地变量的声明
static void Main()
{
int myInt = 15;
SomeClass sc = new SomeClass();
}
5.3.1 var关键字用来类型推断
static void Main(){
var total = 15;
var mec = new MyExcellentClass();
}
第二行中:var 是int的速记
第三行中:var是MyExcellentClass的速记
使用条件:
1.只能用于本地变量,不能用于字段;因为:<mark>字段属于类的 “公开 / 半公开接口”,其类型需要明确暴露(供外部调用者感知),而 `var` 的 “类型推断” 会隐藏类型,破坏接口的清晰度。</mark>
public class MyClass
{
// 错误:var 不能用于字段
var _myField = 10;
public void MyMethod()
{
// 正确:var 用于方法内的本地变量
var localVar = "hello";
if (true)
{
// 正确:var 用于代码块内的本地变量
var blockVar = 3.14;
}
}
}
2.只能在变量声明中包含初始化时使用;<mark>因为:不允许 “先声明 `var` 变量,后赋值”:</mark>
// 错误:var 声明必须同时初始化
var x;
x = 5;
// 正确:声明时直接初始化
var x = 5;
//错误:编译器无法推断 y 是 string?、int?
var y = null
3.一旦编译器推断出变量的类型,它就是固定且不能更改的。<mark>var不是“动态类型”它仅**简化类型声明**,变量的类型在编译时就已确定,运行时无法修改。</mark>
<mark></mark>
var num = 10;
num = "abc";
// 编译器推断 num 为 int 类型,不能将 string 类型赋值给 int 类型的 num
5.3.2 嵌套块中的本地变量

注意:
在C#中不管嵌套级别如何,都不能在第一个名称的有效范围内声明另一个同名的本地变量。
5.4 本地常量
特点:
1.本地常量必须声明在块的内部;
2.声明时必须初始化,且一旦初始化,他的值就不改变了;
3.关键字const不是一个修饰符,而是核心声明的一部分。它必须直接放在类型的前面。

5.5 控制流
术语控制流指的是程序从头到尾的执行流程。
(1)选择语句
if,if...else,switch
(2)迭代语句
for,while,do,foreach
(3)跳转语句
break,continue,goto,return
5.6 方法调用
class MyClass
{
void PrintDateAndTime() //声明方法
{
DateTime dt = DateTime.Now; //获取当前日期和时间
Console.WriteLine("{0}",dt);// 输出
}
static void Main() //声明方法
{
MyClass mc = new MyClass();
mc.PrintDateAndTime(); //调用方法
}
}

5.7 返回值

1.要返回值,方法必须在方法名前面声明一个返回类型;
2.如果方法不返回值,它必须声明void返回类型。
返回语句:包括关键字return及其后面的表达式

5.9 参数
参数用来做两件事:
返回多个值,在方法开始执行的时候把数据传入方法
5.9.1 形参
形参是本地变量,它声明在方法的参数列表中,而不是在方法体中

特点:
1. 因为形参是变量,所以他们有类型和名称,并能被写入和读书
2. 参数在方法体的外面定义并在方法开始之前初始化
<mark>输出参数除外</mark>
3. 参数列表中可以有任意数目的形参声明,而且声明必须用逗号隔开

5.9.2 实参
形参的值必须在方法的代码开始执行之前被初始化,因此
1.<mark>用于初始化形参的表达式或变量称作实参</mark>
2.实参位于方法调用的参数列表中
3.实参和形参的类型得相匹配
4.实参的数量必须和形参的数量一致
5.10 值参数
从方法传入或传出数据的参数,默认为值参数
用处:1.使用值参数,通过将实参的值复制到形参的方式把数据传递给方法。(<mark>如果在方法内修改,其修改的值是副本的值,不会影响原始实参的内存地址,因此方法执行后外部实参仍保持初始值)</mark>
特征:1.值参数的实参不一定是变量。它可以是任何能计算成相应数据类型的表达式.
5.11 引数
特征:
1.使用引用参数时,必须在方法的声明和调用中都<mark>使用ref修饰符</mark>。
2.实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引
用或null。
3.不会为形参在栈上分配内存
4.实际情况是,形参的参数名将作为实参变量的别名,指向相同的内存位置。<mark>所以在方法的执行过程中对形参作的任何改变在方法完成后依然有效(表现在实参变量上)。</mark>
5.12 引用类型作为值参数和引用参数
1.将引用类型对象作为值参数传递 如果在方法内创建一个新对象并赋值给形参,将切断
形参与实参之间的关联,并且在方法调用结束后,<mark>新对象也将不复存在。</mark>
2.将引用类型对象作为引用参数传递 如果在方法内创建一个新对象并赋值给形参,在方
法结束后<mark>该对象依然存在</mark>,并且是实参所引用的值。

1.在方法开始时,实参和形参都指向堆中相同的对象;
2.在为对象的成员赋值之后,它们仍指向堆中相同的对象;
3.当方法分配新的对象并赋值给形参时,(方法外部的)实参仍指向原始对象,而形参指向
的是新对象;
4.在方法调用之后,实参指向原始对象,形参和新对象都会消失。
5.13 输出参数
用法:输出参数用<mark>于从方法体内把数据传出到调用代码</mark>,它们的行为与引用参数非常类似.
特征:
1.必须在声明和调用中都使用修饰符。输出参数的修饰符是out而不是ref;
2.和引用参数相似,实参必须是变量,而不能是其他类型的表达式。<mark>因为方法需要内存位置保存返回值。</mark>
3.与引用参数类似,输出参数的形参担当实参的别名。<mark>因此:方法内对形参的任何改变在方法执行完成之后通过实参变量都是可见的。</mark>
特4:在方法内部,输出参数在能够被读取之前必须被赋值。说明参数的初始值是无关的,<mark>即使未赋值也不影响调用</mark>
特5:在方法返回之前,方法内部贯穿的任何可能路径都必须为所有输出参数进行一次赋值

5.14 参数数组
参数数组,它允许零个或多个实参对应一个特殊的形参。其它参数类型都必须严格地一个实参对应一个形参。
特点:
1. 在一个参数列表中只能有一个参数数组;
2. 如果有,它必须是列表中的最后一个;
3. 由参数数组表示的所有参数都必须具有相同的类型。

声明参数时必须做:
1.在数据类型前<mark>使用params修饰符</mark>
(在声明中需要修饰符。在调用中不允许有修饰符。)
2.在数据类型后放置一组空的方括号,空方括号指明了参数是一个整数数组。
怎么来为参数数组提供实参,有两中方式:
1.一个逗号分隔的该数据类型元素的列表;
2. 一个该数据类型元素的一维数组。、
5.15 参数类型总结

5.16 命名参数

可以看到,只要显式指定参数的名字,就可以以任意顺序在方法调用中列出实参。
5.18 可选参数
顾名思义,我们在调用方法的时候,包含这个参数,也可以省略它。
那怎么表明他是可选函数呢?
我们只需要在方法声明的时候为参数提供默认值


之前我们可以在方法调用的时候省略相应的实参从而为可选参数使用默认值,,但是不可以随意选择省略任意的可选参数,<mark>省略必须从最后开始</mark>,(可以省略最后一个可选参数,或是最后n个可选参数)
5.19 栈帧
我们知道局部变量和参数是位于栈上的,那还有没有其它的组织呢?
其实有块内存,它从栈的顶部开始分配,保存和方法关联的一些数据项,叫做栈帧。
他包含:返回地址、这些参数分配的内存、以及各种和方法调用相关的其他管理数据项。
值得注意的是:
1.在方法调用时,整个栈帧都会压入栈;
2.在方法退出的时候,整个栈帧都会从栈上弹出。

5.20 递归

#### 6.深入理解类
6.1 类成员(9种)

拿英语举例,名词前面有形容词,形容词前有副词
那么,多个修饰词在C#是怎么排列的呢?

注意点:
<mark>修饰符</mark>
如果有修饰符,必须放在核心声明之前。
如果有多个修饰符,可以是任意顺序。
<mark> 特性</mark>
如果有特性,必须放在修饰符和核心声明之前。
如果有多个特性,可以是任意顺序。

6.3 实例类成员
定义:类的每个实例拥有自己的各个类成员的副本
特点:随实例的创建而存在,随实例的销毁而消失,且每个实例的实例成员互不干扰。
6.4 静态字段
特点:
1.静态字段被类的所有实例共享,所有实例都访问同一内存位置。
2.可以使用static修饰符将字段声明为静态。

6.5 从类的外部访问静态成员
我们之前使用点运算符可以从类的外部访问pulic实例成员,那静态成员是不是也可以呢?

重点:只有在实例创建之后才产生实例成员,在实例销毁之后实例成员也就不存在了,但是,静态成员即使没有类的实例也存在。
6.6 静态函数成员
特点:
1.和静态字段差不多,静态函数成员独立于任何类实例。
2.静态函数成员不能访问实例成员。
6.7 其它静态类成员类型

6.8 成员常量
成员常量类似前一章所述的本地常量,只是它们被声明在类声明中而不是方法内
本质:
1.作为类的 “固定属性” 存在
2.编译器在编译代码时,会直接将常量的 “值” 替换到所有引用它的地方
6.9 常量与静态量
1.常量没有自己的存储位置,而是在编译时被编译器替换
2.常量成员表现得像一个静态量,但不能将常量声明为static
6.10 属性
属性是代表类的实例或类中的一个数据项的成员
属性的特征:
1.它是命名的类成员
2.它有类型
3.它可以被赋值和读取
<mark>(然而和字段不同,属性是一个函数成员 4,5)</mark>
4.它不为数据存储分配内存
5.它执行代码
(属性是指定的一组两个匹配的、称为访问器的方法)
6.set访问器为属性赋值
7.get访问器从属性获得值
6.10.1属性声明和访问器
<mark>(访问器决定如何处理发进来的数据,以及什么数据应被发送出去)</mark>
set和get访问器有预定义的语法和语义.
set访问器总是:
1.拥有一个单独的、隐式的值参,名称为value,与属性的类型相同;
2.拥有一个返回类型void.
get访问器总是:
1.没有参数;
2.拥有一个与属性类型相同的返回类型。
属性声明的结构

<mark>注意</mark>:
1.set访问器中的隐式参数value是一个普通的值参,可以用它发送数据到方法体或这种情况中的访问器块,在块的内部可以像普通变量那样使用value,包括对它赋值;set访问器接受它的输入参数value,并把它的值赋给字段TheRealValue。
2.get访问器的所有执行路径必须包含一条return语句,返回一个属性类型的值。set访问器只是返回字段TheRealValue的值。
6.10.3 使用属性

用法:只使用属性名写入和读取属性,就好像它是一个字段名
6.10.4 属性和关联字段
常见的方式是在类中将字段声明为private以封装该字段,并声明<mark>一个public属性</mark>来控制从类的外部对该字段的访问
<mark>值得注意的是:和属性关联的字段常被称为后备字段或后备存储</mark>。
属性和它们的后备字段有几种命名约定:
1.是两个名称使用相同的内容,但字段使用Camel大小写,属性使用Pascal大小写。
2.属性使用Pascal大小写,字段使用相同标识符的Camel大小写版本,并以下划线开始
6.10.5 执行其他计算
访问器get和set能执行任何计算,或不执行任何计算。<mark>唯一必需的行为是get访问器要返回一个属性类型的值。</mark>
6.10.6 只读和只写属性
1.只有get访问器的属性称为只读属性。把一项数据从类或类的实例中传出,而不允许太多访问方法,所以说只读属性是一种安全的
2.只有set访问器的属性称为只写属性。把一项数据从类的外部传入类,而不允许太多访问的方法,只写属性是一种安全的。
3.需要注意的是:<mark>两个访问器中至少有一个必须定义,否则编译器会产生一条错误信息.</mark>

6.10.7 属性与公共字段
为什么说属性比公共字段更好?
1.与公共字段相比,属性是函数成员,不是数据成员,他能够处理输入和输出,但是公共字段不行。
2.属性可以只读或只写,而字段不行;
3.编译后的变量和编译后的属性语意不同
class Student
{
// 私有字段:外部无法直接访问,只能通过属性操作
private int _mathScore;
// 公共属性:控制读写逻辑
public int MathScore
{
get
{
// 读取时可以添加逻辑(如日志记录)
Console.WriteLine("有人读取了分数:" + _mathScore);
return _mathScore;
}
set
{
// 1. 过滤无效数据(解决数据安全问题)
int validScore = value;
if (validScore < 0) validScore = 0; // 负数修正为0
else if (validScore > 100) validScore = 100; // 超分修正为100
// 2. 记录修改日志(解决可维护性问题)
Console.WriteLine($"分数从 {_mathScore} 改为 {validScore},时间:{DateTime.Now}");
// 3. 触发通知(解决扩展性问题)
NotifyParent(validScore);
// 最后赋值给私有字段
_mathScore = validScore;
}
}
// 通知家长的方法
private void NotifyParent(int score)
{
Console.WriteLine($"已通知家长:学生分数更新为 {score}");
}
}
核心:如果你的代码会被其他程序集(比如别人写的项目、你自己的其他模块)引用,那么用公共字段代替属性,未来修改时会带来 “连锁反应”—— 而用属性则能避免这个问题。
举个列子:
我创建Mylibrary项目
namespace MyLibrary
{
public class Student
{
public int MathScore;
}
}
恰巧别的团队的TestApp引用了我的组件
using MyLibrary;
using System;
class Program
{
static void Main()
{
Student stu1 = new Student();
Student stu2 = new Student();
stu1.MathScore = 80;
stu2.MathScore = 90;
int avg = (stu1.MathScore + stu2.MathScore) / 2;
Console.WriteLine($"平均分:{avg}");
}
}

程序运行成功了,但如果我在我的MyLibrary类库中进行修改
namespace MyLibrary
{
public class Student
{
private int _mathScore;
public int MathScore
{
get => _MathScore;
set => _mathScore = Math.Clamp(value,0,100);
}
}
}
这时会弹出运行时错误,因为TestApp是基于“公共字段”编译的,现在调用的是“属性版MyLibrary'''
这时,需要在TestApp项目中,重新编译依赖项.
总结:一开始用字段,后期改成属性,所有引用他的程序必须重新编译:
但如果一开时就用属性,修改内部逻辑时,引用者无需重新编译。
6.10.8 计算只读属性示例
通常大多示例,属性后都和一个后备字段关联,并且get和set访问器引用该字段,<mark>值得注意的是:</mark>属性并非必须和字段关联

6.10.9 自动实现属性
C#提供自动实现属性,它允许只声明属性而不声明后备字段。
因为:编译器会为你创建隐藏的后备字段,并且自动挂接到get和set访问器上

namespace 自动实现属性
{
class Person
{ //没有声明后备字段
public string Name // 分配内存
{ get; set; } //访问器的方法体被声明为分号
public int Age { get; set; }// 分配内存
}
internal class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Name = "Alice";
person.Age = 30;
Console.WriteLine("姓名:{person.Name},年龄:{person.Age}");
//使用自动实现属性
}
}
}
6.10.10 静态属性
<mark>属性也可以声明为static</mark>
静态属性的访问器和所有静态成员一样,那它具有什么特点呢?
<mark>静态属性属于类本身,而非类的实例,所以具备 “不依赖实例、需通过类名访问、不能直接访问实例成员” 等特点</mark>
class Student
{
// 1. 静态属性:存储"学生总人数"(属于类,所有实例共享)
// 编译器自动创建隐藏的静态后备字段(因为是自动实现的静态属性)
public static int TotalStudentCount { get; private set; }
// 2. 实例属性:存储"单个学生的姓名"(属于每个实例,各自独立)
public string Name { get; set; }
// 构造函数:每次创建Student实例时,总人数+1
public Student(string name)
{
Name = name; // 实例属性:给当前实例的姓名赋值
TotalStudentCount++; // 静态属性:修改类级别的总人数(所有实例共享)
}
// 静态方法:重置总人数(静态方法只能访问静态成员)
public static void ResetTotalCount()
{
TotalStudentCount = 0; // 静态方法可以访问静态属性
// Name = "张三"; // 错误:静态方法不能访问实例属性(Name属于实例)
}
}
class Program
{
static void Main()
{
// 特性1:静态属性不依赖实例,即使没有创建任何Student,也能访问(但此时值为0)
Console.WriteLine($"创建学生前,总人数:{Student.TotalStudentCount}"); // 输出:0
// 创建3个Student实例
Student stu1 = new Student("小明");
Student stu2 = new Student("小红");
Student stu3 = new Student("小刚");
// 特性2:访问静态属性必须用"类名.属性名",不能用"实例名.属性名"
Console.WriteLine($"创建3个学生后,总人数:{Student.TotalStudentCount}"); // 正确:类名访问,输出3
// Console.WriteLine(stu1.TotalStudentCount); // 错误(或警告):不能用实例访问静态属性
// 特性3:实例属性必须用"实例名.属性名",不能用"类名.属性名"
Console.WriteLine($"第一个学生姓名:{stu1.Name}"); // 正确:实例访问,输出"小明"
// Console.WriteLine(Student.Name); // 错误:不能用类名访问实例属性
// 重置总人数(静态方法用类名调用)
Student.ResetTotalCount();
Console.WriteLine($"重置后总人数:{Student.TotalStudentCount}"); // 输出:0
}
}
6.11 实例构造函数
这是一个特殊的方法,它在创建类的每一个新实例时执行。
using System;
class Person
{
// 字段(类的状态)
public string Name;
public int Age;
// 实例构造函数①
// ↓ 这是一个public构造函数
public Person(string name, int age)
{
// 构造函数在创建对象时自动执行
Name = name; // 初始化字段
Age = age; // 初始化字段
Console.WriteLine("Person对象被创建了!");
}
// 另一个public构造函数(重载)
public Person()
{
Name = "未知";
Age = 0;
Console.WriteLine("使用默认值创建Person对象");
}
// private构造函数 - 外部无法使用
private Person(string name)
{
Name = name;
Age = 18;
}
}
class Program
{
static void Main()
{
// 可以从外部创建对象,因为构造函数是public
Person person1 = new Person("张三", 25);
Console.WriteLine($"姓名: {person1.Name}, 年龄: {person1.Age}");
// 使用默认构造函数
Person person2 = new Person();
Console.WriteLine($"姓名: {person2.Name}, 年龄: {person2.Age}");
// 无法创建,因为private构造函数外部无法访问
// Person person3 = new Person("李四"); // 编译错误
}
}
6.11.1 带参数的构造函数
class Class1
{
int Id;
string Name;
public Class1() { Id=28; Name="Nemo"; } //构造函数0
public Class1(int val) { Id=val; Name="Nemo"; } //构造函数1
public Class1(String name) { Name=name; } //构造函数2
public void SoundOff()
{ Console.WriteLine("Name {0}, Id {1}", Name, Id); }
}
class Program
{
static void Main()
{
Class1 a = new Class1(), //调用构造函数0
b = new Class1(7), //调用构造函数1
c = new Class1("Bill"); //调用构造函数2
a.SoundOff();
b.SoundOff();
c.SoundOff();
}
}

在使用创建对象表达式创建类的新实例时,要使用new运算符,后面跟着类的某个构造函数。
<mark>new运算符使用该构造函数创建类的实例</mark>
6.11.2 默认构造函数
如果我们写了一个类,完全没写任何构造函数
class Cat
{
public string Name;
public int Age;
}
这时,编<mark>译器会默认给我们加一个无参数、空方法体的构造函数</mark>像
public Cat()
{
}
所以我们接下来可以直接创建对象
Cat myCat = new Cat();
myCat.Name = "咪咪";
myCat.Age = 2;

但是如果
我们手写了一个构造函数
class Dag
{
public string Name;
public int Age;
public Dog(string name,int age)
{
Name = name;
Age = age;
}
}
编译器就不会自动生成默认构造函数

这是系统就会报错,应为没提供所需的参数,只能自己写带参数构造函数
Dog myDog = new Dog("旺财", 3);
Console.WriteLine($"Name:{myDog.Name},Age:{myDog.Age}");

6.12 静态构造函数
作用:静态构造函数是给类本身做初始化的
触发时机:(有两次时刻自行执行)
1.第一次引用类的任何静态成员之前
2.第一次创建类的实例对象之前
与实例构造函数相比
相似点:
1.名称必须和类名相同;
2.不能有返回值。
不同点:(四个方面)
1.关键字:静态构造函数必须加static关键字,而实例构造函数没有static;
2.参数:静态构造函数不带函数,而实例函数可以带函数(用于给"对象的字段"传初始值);
3.数量:类里面只能有一个静态构造函数,而实例构造函数类里可以有多个;
4.访问修饰符:静态构造函数不能有(包括public/private),而实例函数可以有
下面我来个例子
class Game {
// 静态字段:统计所有游戏的总数(类级别的数据)
public static int TotalCount;
// 实例字段:每个游戏的唯一id
public int Id;
// 静态构造函数:初始化“类级别的项”(TotalCount)
static Game() {
TotalCount = 0; // 初始时游戏总数为0
Console.WriteLine("静态构造函数执行了!");
}
// 实例构造函数:初始化“对象级别的项”(Id)
public Game() {
TotalCount++; // 每次创建新游戏,总数+1
Id = TotalCount; // 用总数作为当前游戏的id
Console.WriteLine($"创建了第{Id}个游戏");
}
}
class Program {
static void Main() {
// 第一次创建Game对象时,会先执行“静态构造函数”,再执行“实例构造函数”
Game g1 = new Game();
Game g2 = new Game();
Console.WriteLine($"总游戏数:{Game.TotalCount}");
}
}

第一次 New Game()时,触发静态构造函数:初始化TotaiCount = 0,并打印
然后是执行实例构造函数:TotalCount 加1,第一次创建游戏
第二次New Game()时,静态构造函数不执行了,直接执行的实例构造函数,第二次创建游戏
6.13 对象初始化语句
作用:我们之前创建对象时,要先new出对象,然后在单独给字段/属性
Cat myCat = new Cat();
myCat.Name = "咪咪"
而 对象初始化语句 能让我们在new对象的同时,直接设置字段或属性的值
两种语法形式:(关键区别是否带构造函数参数)
1.不带构造函数参数
通常用于类有默认构造函数,直接在new类名后加{}
class Cat
{
public string Name; // 字段
public int Age; // 字段
// 编译器自动生成默认构造函数(无参、空方法体)
}
class Program
{
static void Main()
{
// 不带构造函数参数的对象初始化:一步创建+赋值
Cat myCat = new Cat { Name = "咪咪", Age = 2 };
// 等价于之前的“两步写法”:
// Cat myCat = new Cat(); // 第一步:创建对象
// myCat.Name = "咪咪"; // 第二步:赋值
// myCat.Age = 2;
Console.WriteLine($"Name: {myCat.Name}, Age: {myCat.Age}"); // 输出:Name: 咪咪, Age: 2
}
}
更多推荐
所有评论(0)