目录
[构造器(构造函数)](#构造器(构造函数))
typeof、default、checked、unchecked、sizeof
一.创建项目

图中解决方案可以包含多个项目,在文件夹中,项目放在解决方案文件夹下,并且会生成一个和项目同级的.sln文件,这个文件是解决方案的管理文件,可以点开后再创建项目
在vs中移除项目,文件夹中并不会删除,当在vs中不小心删除可以在文件夹中找到.sln文件找回
二.c#基础知识
最基础结构:一个类型+一个静态main方法(静态不能去除)

2.1.变量
如输出函数就是写在system里面的方法,要使用这个命名空间下的内容需要在上方,using System,否则只会查找本文件中有无writeLine方法

为什么需要变量并且变量要规定不同变量类型?
- 变量:是为了分配一块空间给用户操作
- 要规定类型是因为,在内存中只有0和1,如果不规定类型例如:56对应asc码的A,而cpu无法知道你是需要A还是56,变量的类型是给了这块内存一个标签,存放的哪种类型,准确的拿到结果
- 在进行数据的运算时,赋值号左右的类型要相同
2.2数据类型、变量和对象
c#的数据类型分为2种五类,图中蓝色的为关键字同时也是数据类型,也就是说太常用c#把他作为关键字,当在vs中输入int 并且点击f12,可以看到int为值类型里面的结构体类型
值类型是没有实例的
引用类型的引用变量是存储实例内容地址的地址

2.2.1数据类型和变量类型的关系
cs
int age = 25; // 变量 age 的类型是 int(变量类型),而 int 本身是数据类型
string name = "张三"; // 变量 name 的类型是 string(变量类型),string 是数据类型
- 说 "数据类型" 时,更侧重 "数据本身的特性"(如
int
能存多大的数); - 说 "变量类型" 时,更侧重 "变量作为容器的特性"(如 "这个变量是
int
类型的,所以只能存整数")
2.2.2变量类型

小内存的数据放在大内存的数据类型里会浪费空间
大内存的数据放小内存的数据类型会丢失精度
当程序运行时,数据类型放在那里?
- 当程序运行时,程序从硬盘放到内存中,数据类型也在内存中
- 方法放在内存的栈区,方法太多会找出栈溢出,对象放在内存的堆区,堆的内存未回收会内存泄漏,但是c#会有垃圾回收机制,当没有调用这个对象时会自动回收。
- 局部变量也放在栈上
- 类中的成员变量放在堆上
2.2.3常量
- 使用const关键字
- 常量的值不能被修改
- 常量必须在定义时赋初值
- 常量作为成员常量时是类型的成员,即和静态变量相同,通过类型访问
cs
const int a = 10;
2.3程序框的输入输出
console.readline(命令框输入)、console,writeline(打印)
其中输入/输出命令框的格式都是字符串类型,因此writeline可以使用+实现字符串的拼接;readline只能用字符串接受输入数据
输出一个拼接字符串
cs
string name = "john";
string age = "20";
Console.WriteLine("{0}is{1}", name, age);
三、语句
语句只能出现在方法体中
语句以;结尾
3.1.声明语句
3.1.1.变量声明语句
cs
int a=100;
//声明在赋值;
int b;
b=100;
//数值使用初始化器
int [] array={1,2}
图中:a后面的就是变量初始化器,而{1,2}是数值的初始化器
3.1.2常量声明语句
cs
const int a=100
图中:常量必须要被初始化,且值不能再发生改变.
3.2标签语句
cs
hell0: Console.WriteLine("hello");
goto hell0;
给语句添加标签,可以在goto中直接跳转到该语句,上面代码会一直执行.
3.3条件语句
3.3.1.条件运算符
Exp1 ? Exp2 : Exp3;
Exp1是否为真,若为真则执行条件Exp2,为假则执行Exp3
3.3.2.if/else语句
cs
int a=10;
int b=5;
if(a>b){
Console.WriteLine("hello");
}
if(a>b)
Console.WriteLine("hello");
if语句的()只能放布尔表达式
if语句只能有一条嵌入式语句,因此有时只需要一条语句时可以不加{},而{}括起来的部分,称为块语句,编译器将块语句中内容视为一条语句;
在vs中输入if点击两下Tab键直接创建代码结构
3.3.3.switch语句
cs
using System;
namespace MyApplication
{
class Program
{
static void Main(string[] args)
{
int day = 4;
switch (day) //这里的day就是expression
{
case 1:
Console.WriteLine("Monday");
break;
case 2:
Console.WriteLine("Tuesday");
break;
case 3:
Console.WriteLine("Wednesday");
break;
case 4:
Console.WriteLine("Thursday");
break;
case 5:
Console.WriteLine("Friday");
break;
case 6:
Console.WriteLine("Saturday");
break;
case 7:
Console.WriteLine("Sunday");
break;
}
}
}
}
- switch 语句中的 expression 必须是一个整型或枚举类型,或者是一个 class 类型,其中 class 有一个单一的转换函数将其转换为整型或枚举类型,没有浮点类型
- case后面的数必须和 switch 中的变量具有相同的数据类型,且必须是一个常量
- break必须每一个case都要有
- 可以选择在增加default 作为默认
- 并且如果两个case在逻辑上是相同范围,连起来写就行了,下面例子中如果case 8:后面写了表达式那么久必须加break,因为他变成了一个分支
3.3.4枚举类型和switch搭配
cs
Level level = Level.Low;
switch (level)
{
case Level.Low:
break;
case Level.Mid:
break;
case Level.High:
break;
default:
break;
}
}
enum Level { Low, Mid, High }
枚举类型的定义要和类的定义写在一个层面
快捷划分枚举中常量,输入switch,两下tab键,在条件中输入枚举变量level,回车自动生成.
3.3.5try语句
使用try语句尝试执行,若有错误,就不执行try语句块中内容,执行catch中内容
catch语句是分为有条件的和无条件的
下面代码若调用函数时输入不是数值形式的字符串try中内容执行时有错误,就不会改变a,b的值,并且执行catch语句,输出报错了
finally语句无论是否有错误都会执行
cs
class jisuan
{
int a = 0;
int b = 0;
public double Add(string num1, string num2)
{
try
{
a = int.Parse(num1);
b = int.Parse(num2);
Console.WriteLine("a+b");
}
catch
{
Console.WriteLine("报错了");
}
return a + b;
finally{
}
}
}
3.4循环语句
3.4.1for循环/while循环/do-while语句
和c语言同
3.4.2foreach循环
1.使用迭代器来实现遍历集合
如下图:可以看到我们定义的数组输入Array类(所有定义的数组都属于这个类)
cs
int[] array = new int[5] { 1, 2, 3,4,5 };
Console.WriteLine(array.GetType().FullName);//类型名称
Console.WriteLine(array is Array);//判断是否属于Array类
右键Array转到定义,所有的I开头的都是接口,所有实现了IEnumberable接口的类的实例都是可以被遍历的集合
cs
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable
右键IEnumberable接口,可以看到迭代器方法,所有可以被迭代的集合都能获得迭代器
cs
IEnumerator GetEnumerator();
使用迭代器来实现迭代,定义了一个迭代器接口类型=定义的泛型返回的迭代器,调用迭代器的MoveNext方法判断是否能向后移动(读到6就不能移动了),Reset方法回到原点,.Current属性为当前值

2.使用for-each语句来实现变量集合
for-each语句就是迭代器的简便写法
cs
int[] array = new int[5] { 1, 2, 3,4,5 };
foreach (var item in array)
{
Console.WriteLine(item);
}
3.5方法
c#中不能直接在命名空间下写函数,而必须作为某一个类的方法(如下图中的方法)
c#中方法的声明和定义是不分家的(如下图)
cs
namespace ConsoleApp1
{
class Hero
{
public string Name;
}
class Circle
{//圆的面积
public double Math_Circle(double r){
double area=3.14*r*r;
return area;
}
//圆柱体体积
public static double Computer_cylinder(double area,double h)
{
double Volume = area * h;
return Volume;
}
}
class Program
{
static void Main(string[] args)
{
Circle C = new Circle();
//调用计算面积方法
double area = C.Math_Circle(2);
//调用计算圆柱体体积方法
double volume = Circle.Computer_cylinder(area, 3);
Console.WriteLine();
}
}
}
静态方法和实例方法
上图中的圆面积的计算方法就是实例方法,通过实例来调用,需要先把类示例化
上图中的圆柱体体积的计算方法就静态方法,通过类名来调用
构造器(构造函数)
每个声明的类都会有一个默认的构造器函数,代码如下
方法的重载
声明带有重载的方法,方法签名是由方法的名称,类型形参的个数、形参的个数、顺序、类型、种类来区分的,不包含返回类型
补充:(后面补充)
参数的种类:分为传值(普通形式),传输出(参数前加out),传引用(参数前加ref)
类型形参:泛型中内容,在函数名后<类型形参名>,可以实现在函数中定义类型形参名那种种类的参数
cs
class Student
{
public Student(string name, int age)
{
this.name = name;
this.age = age;
}
//函数重载
public Student(string name, String age)
{
this.name = name;
this.age = int.Parse(age);
}
public string name;
public int age;
}
这个实例:是重载又是构造器
函数参数
值参数:
- 在传递时方法体里面会创建一个副本
- 实参和形式参数是值传递的,形式参数是实际参数的副本,两者地址不同但是存储相同数据
- 当然:引用类型作为参数传入函数,形参和实参地址不同但是都指向相同地址,下例
cs
static void Main(string[] args)
{
Hero hero=new Hero() { Name="Superman"};
Somemethod(hero);引用类型作为值传递给形式参数
}
static void Somemethod(Hero hero) {
Console.WriteLine(hero.Name);
}
引用参数:
使用ref关键字,不创建副本,直接将值的地址传进函数。
注意:在形参和实参位置都要加ref关键字
使用引用类型来作为引用参数也是一样的,不会创建副本,使用相同地址并且指向相同对象;
下例:值类型:y的数据也被改变,因为x和y实际是同一个地址
cs
static void Main(string[] args)
{
int y = 100;
refdemo(ref y);
Console.WriteLine(y);//输出结果为110
}
static void refdemo(ref int x)
{
x += 10;
}
输出参数:
输出参数在方法体里面必须要赋值
实际参数可以只声明不赋值,值传入时被丢弃掉了
使用out关键字,相当于多一个返回值,不创建副本,和ref的区别为实际参数可以不赋值,但是在函数内必须为形式参数赋值
cs
static void Main(string[] args)
{
int y = 100;
refdemo(out y);
Console.WriteLine(y);
}
static void refdemo(out int x)
{
x = 10;//必须要赋值
}
数组参数:
params参数必须是参数列表的最后一个参数
不使用数组参数,将数组作为实际参数传入函数
cs
static void Main(string[] args)
{
int[] arr = new int[5] { 1, 2, 3, 4, 5 };
each(arr);
}
static void each( int[] arr)
{
foreach (int i in arr)
{
Console.WriteLine(i);
}
}
使用数组参数params,不需要定义一个数组,直接输入值
cs
static void Main(string[] args)
{
each(1,2,3,4,5);
}
static void each(params int[] arr)
{
foreach (int i in arr)
{
Console.WriteLine(i);
}
具名参数(本质是使用方法)
可读性高
不受参数输入顺序限制
输入类型是键对值形式,键名要和形参一致
cs
static void Main(string[] args)
{
student(age: 20, name: "sun");
}
static void student(string name,int age){
Console.WriteLine("{0} is {1} years",name,age);
}
可选参数
在声明时给默认值,不输入对应参数就使用默认值
cs
static void Main(string[] args)
{
student();
}
static void student(string name="sun",int age=18){
Console.WriteLine("{0} is {1} years",name,age);
}
扩展参数
扩展方法必须写在静态类中,类名约定为SomeTypeExtension
必须是形参列表中第一个参数,需要对那个类型做扩展,用this修饰这个类型
方法必须是公有的,静态的
扩展方法是增加目标类型函数功能的,下面我们为double类型添加一个保留4位小数的功能
cs
internal class Program
{
static void Main(string[] args)
{
double x = 2.123456;
double y = x.Round(3);
Console.WriteLine(y);//输出结果位2.123
}
}
//扩展方法必须写在静态类中,类名约定为SomeTypeExtension
static class DoubleExtension
{//方法必须是公有的,静态的
public static double Round(this double num, int dec)//必须是形参列表中第一个参数,需要对那个类型做扩展,用this修饰这个类型
{
num = Math.Round(num, dec);
return num;
}
}
数组
数组的写法和c语言不一致【】放在名称前类型后,顺便在这里做声明和定义的区分
并且在定义时要给他分配空间
1.声明数组
cs
datatype[] arrayName;//这里的datatype是数据类型
//声明一个int型数组
int[] arr1;
2.定义数组
(已声明的数组变量分配内存空间,并确定数组的长度(元素个数))
cs
分配空间并且规定数组大小
arr1 = new int[3];
3.初始化数组
3.1动态初始化:先定义长度,再指定初始值
cs
// 定义长度为3的int数组,并初始化元素值(首次赋值)
int[] arr3 = new int[3] { 10, 20, 30 };
3.2静态初始化
cs
// 省略长度,编译器自动根据初始值个数(4个)定义长度为4,并初始化
int[] arr4 = new int[] { 1, 2, 3, 4 };
// 更简化的写法(编译器会自动补全new int[])
int[] arr5 = { 5, 6, 7 };
操作符
基本概念:
操作符不能脱离他关联的数据类型(整数除法和小数除法都是/,但是结果不同)
操作符优先级:
使用圆括号可以提示表达式优先级,圆括号可以嵌套
带有赋值操作符的运算顺序是先算运算符右边
除了带有赋值运算的操作符,其他都是从左到右

new操作符
new操作符:
1.创建实例,并且调用实例构造器,还能获得实例的地址,下例:new操作符创建了Hero实例,并且通过()调用了构造器,再将实例地址传给了引用变量hero;
cs
Hero hero1 = new Hero();
2.调用初始化器(使用场景:只输入参数创建一个实例使用里面的一个函数,然后不使用引用参数获取他的地址,如下面例子,不使用引用参数获取他的值时,调用完成后内存被回收)
cs
// 传统方式:先 new 再赋值
Person p1 = new Person();
p1.Name = "张三";
p1.Age = 25;
p1.Address = "北京市";
// new + 对象初始化器:一步完成创建和初始化
new Person
{
Name = "李四", // 初始化属性
Age = 30,
Gender = "男",
Address = "上海市" // 初始化公共字段
}.show();
3.给匿名类型创建对象(这个类是没有创建的,因此声明类型时间用var,参数就是输入的Id这些)
cs
var student = new
{
Id = 1001,
Name = "赵六",
Score = 95.5
};
Console.WriteLine(student.Name)
typeof、default、checked、unchecked、sizeof
typeof:获取数据类型
default:获取默认值
checked():检查到溢出会报错,可以checked{},在{}内的代码都会被检查
unchecked():不检查是否会溢出,默认设置,使用方式和checked一样
sizeof:只能获取结构体类型所占字节的大小
->操作符:间接访问,只能放在不安全上下文使用(使用unsafe并且在上方项目->项目属性->生成->勾选允许不安全上下文),在c#中指针操作,取地址操作,使用指针访问成员只能只能操作结构体类型,不能访问引用类型
cs
struct Sport
{
public String name;
public int score;
}
static void Main(string[] args)
{
unsafe {
Sport spo;
spo.score = 100;
spo.name = "football";
Sport *psport = &spo;
psport->score = 99;
Console.WriteLine(spo.score);
}
}
一元操作符
*和&:指针符号和取地址符号
+、-、~操作符:可以对数值取正和负,注意:计算机中同一类型的最大最小值是不对称的,负值绝对值通常大一,因此使用+,-操作可能会导致内存溢出。
~操作符:在二进制意义上每一位取反
!:取非
++、--:和c相同,在赋值时,在变量前后结果有区别
(T)x:类型转换,看2.10类型转换详解
乘法运算符(*)
这里要注意,乘法运算符的类型是由参与运算的变量类型来决定的,如果变量有高精度会按照高精度计算
cs
int x=10;
int y=3;
int t=3;
int result=x/y; //结果是3
double result1=x/t //结果是3.33333
位移操作符(>>,<<)
数据的二进制数,左位移和右位移,由于二进制的特性,不产生溢出情况:左移就是×2,右移就是/2,
位移操作符补位规则:
左移:全部补0;右移:正数补0,负数补1;
关系操作符
知识引入:
- 编译时类型 :变量声明时的类型(这里
a
和a1
的编译时类型都是Animal
)在声明时已经确定。 - 运行时类型 :变量实际指向的对象类型(
a
指向Human
对象,a1
指向Animal
对象)运行时类型在使用new创建时已经固定了
is:检验实例是否属于类,是返回true,失败返回false
as关键字既可以检验是否是属于该类,又可以实现转换引用变量类型;
转换规则是:只有当源对象的 "实际运行时类型" 是目标类型,或目标类型的派生类型时,转换才会成功,下文中a的实际运行类型就是human,而a1的实际运行类型是animal,在使用(引用变量 as human)时只有a可以成功,返回一个等于实际类型的编译时类型,下面代码只是返回了一个human类的引用变量,而a的编译类型并没有改变.
在继承关系中,子类实例可以安全转换为父类类型 (向上转型:直接子类赋值给父类,如 Human
→ Animal
),但父类实例不能直接转换为子类类型 (向下转型,如 Animal
→ Human
),除非父类变量实际指向的是子类实例(如 a
指向 Human
实例)
注意:父类变量可以指向子类实例,子类变量却无法指向父类实例
cs
human h = new human();
if (h is human)
{
h.thike();//使用is运算符判断这个引用变量是否为这个类的实例
}
human h1 = new human();
if (h1 is human)
{
h1.Eat();//使用is运算符判断子类的引用变量也可以视为父类的实例
}
animal a = new human();//父类变量指向子类实例
animal a1 = new animal();//这种编译类型和实际类型都是animal的就不能父类转换为子类
human h2=a as human;//转换仅改变引用该对象的变量的编译时类型这里是h2------ 从基类(Animal)变为子类(Human),从而解锁对 "子类独有成员" 的直接访问
if (h2 != null)
{
h2.thike();
}
与或非逻辑操作符
&:按位与,是看二进制位,1和1才是真其他为假
|:按位或,有一个为真即为真
^:两位不一样才是真
条件与,条件或
||:或,操作对象和结果为布尔值
&&:与,操作对象和结果为布尔值
**短路效应:**当已经可以判断出结果时,后面的内容就会被跳过,如下:a>b为假,c++不会运行,c的值不变,最外层结构为A||B时,A为真则跳过判断B,最外层结构为A&&B时,A为假就会跳过A
cs
int a = 10;
int b = 20;
int c = 10;
if (a > b && c++ > 10)
{
}
Console.WriteLine(c);
?操作符
举例说明:
cs
int? a = null;
var x = a?? 10;
上面代码
int ?a=null含义为定义一个可空的int类型元素a=null(int?是语法糖,全写为 Nullable<int>
)
??是空合并运算符,若a为空返回10,若不为空返回a的值
2.10类型转换
2.10.1.隐式类型转换(直接赋值)
不丢失精度的类型转换:低精度向高精度转换
子类向父类的类型转换:直接把子类引用类型赋值给父类的引用类型,注意:转换后的父类引用类型只能访问他自己有的成员方法和成员变量
cs
static void Main(string[] args)
{
human h = new human();
animal a = h;
//直接将h赋值给父类的引用类型,注意,父类只能调用他自己有的方法和属性
a.Eat();
}
}
class animal
{
public void Eat()
{
Console.WriteLine("animal is eating");
}
}
class human : animal {
public void thike() {
Console.WriteLine("human is thiking");
}
}
显式类型转换
使用类型转换关键字,一般是同类型数据高精度向低精度转换
cs
double x = 3.14159;
int y = (int)x; // 显式转换:double → int(小数部分被截断,y=3)
特别的是char和整数之间的转换,但是因为字符对应码表的原因,可以相互转换
cs
char c = 'A';
int code = (int)c; // 结果:65('A'的Unicode编码)
注意:这种类型转换是可能造成溢出的,而double转换为int类型不显示小数部分是截断因为int就是表示的整数,不是一个概念
**convert类:**直接在编译器.来查看有那些方法
Tostring方法: 这个方法是写在object类中的在上文数据类型中可以看到,所有的数据类型都是他的派生,因此都有Tostring方法,Tostring是实例方法,通过实例调用
cs
int x = 20;
x.ToString();
Parse方法 :将字符串类型转换为目标类型,Parse是静态方法,通过类名称调用,注意:只能转换格式正确的,如:123转换为int类型
cs
// int.Parse:字符串转整数
string intStr = "123";
int num = int.Parse(intStr); // 转换成功,num = 123
// double.Parse:字符串转双精度浮点数
string doubleStr = "3.1415";
double pi = double.Parse(doubleStr); // 转换成功,pi = 3.1415
// decimal.Parse:字符串转高精度小数(适合金额)
string decimalStr = "199.99";
decimal price = decimal.Parse(decimalStr); // 转换成功,price = 199.99
类和对象
类怎么定义和实例化
不调用实例构造器方式
cs
class Hero
{
public string Name;
}
class Program
{
static void Main(string[] args)
{
//这里的hero1叫引用变量
Hero hero1 = new Hero();
hero1.Name = "Superman";
Console.WriteLine("Hello, " + hero1.Name + "!");
}
}
调用实例构造器方式
cs
static void Main(string[] args)
{
Hero hero=new Hero() { Name="Superman"};
}
class Hero
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
修饰限定符及其默认值
C# 封装根据具体的需要,设置使用者的访问权限,并通过 访问修饰符 来实现。
一个 访问修饰符 定义了一个类成员的范围和可见性。C# 支持的访问修饰符如下所示:
- public:所有对象都可以访问;
- private:只在类的内部可以访问,即使是类的实例也不能访问它的私有成员;
- protected:只有该类对象及其子类对象可以访问
- internal:同一个程序集的对象可以访问;
- protected internal:访问限于当前程序集或派生自包含类的类型。

- 顶层类型指直接定义在
namespace
中的类、结构体、接口、枚举、委托等,其默认访问修饰符为internal
。 - 嵌套类型指定义在类、结构体内部的类型(如类中的嵌套类),其默认访问修饰符为
private
- 类(
class
)和结构体(struct
)的成员(字段、方法、属性、事件、构造函数等),默认访问修饰符为private
类的成员
属性
作用:是为了避免数据污染,属性是字段的包装器。
类属性是有默认值的,而写在progress类中的mian方法里面的变量称为本地变量,本地变量是没有默认值的
属性的完整写法
快捷键写法:输入propfull(prop是property的缩写),按两下tab键,使用tab键选择给我们修改的地方,快速生成
注意:
属性的set和get是没有();
调用set方法时value是不需要自己定义的,等于给他赋的值
cs
private int age;
public int Age
{
get
{
return this.age;
}
set
{
if (value >= 0 && value <= 120)
{
this.age = value;
}
else
{
throw new Exception("age value is error");
}
}
}
字段(成员变量)
静态字段和实例字段
静态字段用来描述一个类型共有的属性,实例字段描述一个实例单独的属性
静态字段和类同级,无论多少个实例,一个类中同名静态字段只有一个
静态字段通过类名调用,实例字段通过实例调用
cs
static void Main(string[] args)
{
Student stu=new Student();
stu.name = "John";
stu.age = 20;
Student stu2 = new Student();
stu2.name = "Mary";
stu2.age = 22;
double average_age = Student.average_age=(stu.age+stu2.age)/ (double)2;
Console.WriteLine(average_age);
}
class Student
{
public static double average_age;
public string name;
public int age;
}
this关键字
this
只能在实例成员 (实例方法、实例属性、实例构造函数)中使用,静态成员 (静态方法、静态构造函数)中不能使用this
(因为静态成员属于类,不属于某个实例)this
的核心是 "当前对象的引用",就是指向当前对象的指针,它的主要作用是:区分同名成员与局部变量(加this的是成员)、传递当前对象、调用其他构造函数、定义扩展方法。this
写在类的定义中,是 **"提前声明" 了一种 "对当前实例的引用方式"**,但此时还没有具体的实例,所以它是 "抽象的占位符"。- 当通过
new
创建实例后(比如zhangsan = new Person()
),这个实例在内存中存在了。此时调用实例的方法(zhangsan.SayHello()
),this
就会动态绑定到这个具体实例 (zhangsan
),所以this.Name
才能正确拿到 "张三