c#基础(一)
目录
一.创建项目
二.c#基础知识
2.1.变量
2.2数据类型、变量和对象
2.2.1数据类型和变量类型的关系
2.2.2变量类型
2.2.3常量
2.3程序框的输入输出
三、语句
3.1.声明语句
3.1.1.变量声明语句
3.1.2常量声明语句
3.2标签语句
3.3条件语句
3.3.1.条件运算符
3.3.2.if/else语句
3.3.3.switch语句
3.3.4枚举类型和switch搭配
3.3.5try语句
3.4循环语句
3.4.1for循环/while循环/do-while语句
3.4.2foreach循环
1.使用迭代器来实现遍历集合
2.使用for-each语句来实现变量集合
3.5方法
静态方法和实例方法
构造器(构造函数)
方法的重载
函数参数
值参数:
引用参数:
输出参数:
数组参数:
具名参数(本质是使用方法)
可选参数
扩展参数
数组
1.声明数组
2.定义数组
3.初始化数组
操作符
基本概念:
new操作符
typeof、default、checked、unchecked、sizeof
一元操作符
乘法运算符(*)
位移操作符(>>,<<)
关系操作符
与或非逻辑操作符
条件与,条件或
?操作符
2.10类型转换
2.10.1.隐式类型转换(直接赋值)
显式类型转换
类和对象
类怎么定义和实例化
修饰限定符及其默认值
类的成员
属性
字段(成员变量)
静态字段和实例字段
this关键字
一.创建项目
图中解决方案可以包含多个项目,在文件夹中,项目放在解决方案文件夹下,并且会生成一个和项目同级的.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数据类型和变量类型的关系
int age = 25; // 变量 age 的类型是 int(变量类型),而 int 本身是数据类型
string name = "张三"; // 变量 name 的类型是 string(变量类型),string 是数据类型
- 说 “数据类型” 时,更侧重 “数据本身的特性”(如
int
能存多大的数); - 说 “变量类型” 时,更侧重 “变量作为容器的特性”(如 “这个变量是
int
类型的,所以只能存整数”)
2.2.2变量类型
小内存的数据放在大内存的数据类型里会浪费空间
大内存的数据放小内存的数据类型会丢失精度
当程序运行时,数据类型放在那里?
- 当程序运行时,程序从硬盘放到内存中,数据类型也在内存中
- 方法放在内存的栈区,方法太多会找出栈溢出,对象放在内存的堆区,堆的内存未回收会内存泄漏,但是c#会有垃圾回收机制,当没有调用这个对象时会自动回收。
- 局部变量也放在栈上
- 类中的成员变量放在堆上
2.2.3常量
- 使用const关键字
- 常量的值不能被修改
- 常量必须在定义时赋初值
- 常量作为成员常量时是类型的成员,即和静态变量相同,通过类型访问
const int a = 10;
2.3程序框的输入输出
console.readline(命令框输入)、console,writeline(打印)
其中输入/输出命令框的格式都是字符串类型,因此writeline可以使用+实现字符串的拼接;readline只能用字符串接受输入数据
输出一个拼接字符串
string name = "john";
string age = "20";
Console.WriteLine("{0}is{1}", name, age);
三、语句
语句只能出现在方法体中
语句以;结尾
3.1.声明语句
3.1.1.变量声明语句
int a=100;
//声明在赋值;
int b;
b=100;
//数值使用初始化器
int [] array={1,2}
图中:a后面的就是变量初始化器,而{1,2}是数值的初始化器
3.1.2常量声明语句
const int a=100
图中:常量必须要被初始化,且值不能再发生改变.
3.2标签语句
hell0: Console.WriteLine("hello");goto hell0;
给语句添加标签,可以在goto中直接跳转到该语句,上面代码会一直执行.
3.3条件语句
3.3.1.条件运算符
Exp1 ? Exp2 : Exp3;
Exp1是否为真,若为真则执行条件Exp2,为假则执行Exp3
3.3.2.if/else语句
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语句
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搭配
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语句无论是否有错误都会执行
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类(所有定义的数组都属于这个类)
int[] array = new int[5] { 1, 2, 3,4,5 };Console.WriteLine(array.GetType().FullName);//类型名称Console.WriteLine(array is Array);//判断是否属于Array类
右键Array转到定义,所有的I开头的都是接口,所有实现了IEnumberable接口的类的实例都是可以被遍历的集合
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable
右键IEnumberable接口,可以看到迭代器方法,所有可以被迭代的集合都能获得迭代器
IEnumerator GetEnumerator();
使用迭代器来实现迭代,定义了一个迭代器接口类型=定义的泛型返回的迭代器,调用迭代器的MoveNext方法判断是否能向后移动(读到6就不能移动了),Reset方法回到原点,.Current属性为当前值
2.使用for-each语句来实现变量集合
for-each语句就是迭代器的简便写法
int[] array = new int[5] { 1, 2, 3,4,5 };
foreach (var item in array)
{Console.WriteLine(item);
}
3.5方法
c#中不能直接在命名空间下写函数,而必须作为某一个类的方法(如下图中的方法)
c#中方法的声明和定义是不分家的(如下图)
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)
类型形参:泛型中内容,在函数名后<类型形参名>,可以实现在函数中定义类型形参名那种种类的参数
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;}
这个实例:是重载又是构造器
函数参数
值参数:
- 在传递时方法体里面会创建一个副本
- 实参和形式参数是值传递的,形式参数是实际参数的副本,两者地址不同但是存储相同数据
- 当然:引用类型作为参数传入函数,形参和实参地址不同但是都指向相同地址,下例
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实际是同一个地址
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的区别为实际参数可以不赋值,但是在函数内必须为形式参数赋值
static void Main(string[] args)
{int y = 100;refdemo(out y);Console.WriteLine(y);
}static void refdemo(out int x)
{x = 10;//必须要赋值
}
数组参数:
params参数必须是参数列表的最后一个参数
不使用数组参数,将数组作为实际参数传入函数
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,不需要定义一个数组,直接输入值
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);}
具名参数(本质是使用方法)
可读性高
不受参数输入顺序限制
输入类型是键对值形式,键名要和形参一致
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);
}
可选参数
在声明时给默认值,不输入对应参数就使用默认值
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位小数的功能
internal class Program{static void Main(string[] args){double x = 2.123456;double y = x.Round(3);Console.WriteLine(y);//输出结果位2.123}}
//扩展方法必须写在静态类中,类名约定为SomeTypeExtensionstatic class DoubleExtension{//方法必须是公有的,静态的public static double Round(this double num, int dec)//必须是形参列表中第一个参数,需要对那个类型做扩展,用this修饰这个类型{num = Math.Round(num, dec);return num;}}
数组
数组的写法和c语言不一致【】放在名称前类型后,顺便在这里做声明和定义的区分
并且在定义时要给他分配空间
1.声明数组
datatype[] arrayName;//这里的datatype是数据类型//声明一个int型数组int[] arr1;
2.定义数组
(已声明的数组变量分配内存空间,并确定数组的长度(元素个数))
分配空间并且规定数组大小
arr1 = new int[3];
3.初始化数组
3.1动态初始化:先定义长度,再指定初始值
// 定义长度为3的int数组,并初始化元素值(首次赋值)
int[] arr3 = new int[3] { 10, 20, 30 };
3.2静态初始化
// 省略长度,编译器自动根据初始值个数(4个)定义长度为4,并初始化
int[] arr4 = new int[] { 1, 2, 3, 4 }; // 更简化的写法(编译器会自动补全new int[])
int[] arr5 = { 5, 6, 7 };
操作符
基本概念:
操作符不能脱离他关联的数据类型(整数除法和小数除法都是/,但是结果不同)
操作符优先级:
使用圆括号可以提示表达式优先级,圆括号可以嵌套
带有赋值操作符的运算顺序是先算运算符右边
除了带有赋值运算的操作符,其他都是从左到右
new操作符
new操作符:
1.创建实例,并且调用实例构造器,还能获得实例的地址,下例:new操作符创建了Hero实例,并且通过()调用了构造器,再将实例地址传给了引用变量hero;
Hero hero1 = new Hero();
2.调用初始化器(使用场景:只输入参数创建一个实例使用里面的一个函数,然后不使用引用参数获取他的地址,如下面例子,不使用引用参数获取他的值时,调用完成后内存被回收)
// 传统方式:先 new 再赋值Person p1 = new Person();p1.Name = "张三";p1.Age = 25;p1.Address = "北京市";// new + 对象初始化器:一步完成创建和初始化new Person{Name = "李四", // 初始化属性Age = 30, Gender = "男",Address = "上海市" // 初始化公共字段}.show();
3.给匿名类型创建对象(这个类是没有创建的,因此声明类型时间用var,参数就是输入的Id这些)
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#中指针操作,取地址操作,使用指针访问成员只能只能操作结构体类型,不能访问引用类型
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类型转换详解
乘法运算符(*)
这里要注意,乘法运算符的类型是由参与运算的变量类型来决定的,如果变量有高精度会按照高精度计算
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
实例)
注意:父类变量可以指向子类实例,子类变量却无法指向父类实例
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
int a = 10;int b = 20;int c = 10;if (a > b && c++ > 10){}Console.WriteLine(c);
?操作符
举例说明:
int? a = null;var x = a?? 10;
上面代码
int ?a=null含义为定义一个可空的int类型元素a=null(int?是语法糖,全写为 Nullable<int>
)
??是空合并运算符,若a为空返回10,若不为空返回a的值
2.10类型转换
2.10.1.隐式类型转换(直接赋值)
不丢失精度的类型转换:低精度向高精度转换
子类向父类的类型转换:直接把子类引用类型赋值给父类的引用类型,注意:转换后的父类引用类型只能访问他自己有的成员方法和成员变量
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");}}
显式类型转换
使用类型转换关键字,一般是同类型数据高精度向低精度转换
double x = 3.14159;
int y = (int)x; // 显式转换:double → int(小数部分被截断,y=3)
特别的是char和整数之间的转换,但是因为字符对应码表的原因,可以相互转换
char c = 'A';
int code = (int)c; // 结果:65('A'的Unicode编码)
注意:这种类型转换是可能造成溢出的,而double转换为int类型不显示小数部分是截断因为int就是表示的整数,不是一个概念
convert类:直接在编译器.来查看有那些方法
Tostring方法:这个方法是写在object类中的在上文数据类型中可以看到,所有的数据类型都是他的派生,因此都有Tostring方法,Tostring是实例方法,通过实例调用
int x = 20;x.ToString();
Parse方法:将字符串类型转换为目标类型,Parse是静态方法,通过类名称调用,注意:只能转换格式正确的,如:123转换为int类型
// 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
类和对象
类怎么定义和实例化
不调用实例构造器方式
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 + "!");}}
调用实例构造器方式
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是不需要自己定义的,等于给他赋的值
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");}}}
字段(成员变量)
静态字段和实例字段
静态字段用来描述一个类型共有的属性,实例字段描述一个实例单独的属性
静态字段和类同级,无论多少个实例,一个类中同名静态字段只有一个
静态字段通过类名调用,实例字段通过实例调用
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
才能正确拿到 “张三