C#
简介
**C#**是微软公司发布的一种由C和C++衍生出来的面向对象的编程语言,它不仅去掉了 C++ 和 Java 语言中的一些复杂特性,还提供了可视化工具,能够高效地编写程序。
**C#**是由C和C++衍生出来的一种安全的、稳定的、简单的、优雅的面向对象编程语言。它在继承C和C++强大功能的同时去掉了一些它们的复杂特性(例如没有宏以及不允许多重继承)。
**C#**使得C++程序员可以高效的开发程序,且因可调用由 C/C++ 编写的本机原生函数,而绝不损失C/C++原有的强大的功能。因为这种继承关系,C#与C/C++具有极大的相似性,熟悉类似语言的开发者可以很快的转向C#。
一.C#入门
1.输入输出
//读取用户的输出,返回一个int类型
Console.Read();
//读取用户的输入,返回一个string类型
Console.ReadLine();
//输出数据
Console.Write("Hello world");
//输出数据并换行
Console.WriteLine("Hello world");
//读取用户输入,多用于暂停程序
Console.ReadKey();
2.变量
C# 中变量定义的语法:
int i, j, k;
char c, ch;
float f, salary;
double d;
变量定义时进行初始化:
int i = 100;
3.常量
- 常量是固定值,程序执行期间不会改变。常量可以是任何基本数据类型,比如整数常量、浮点常量、字符常量或者字符串常量,还有枚举常量。
- 常量可以被当作常规的变量,只是它们的值在定义后不能被修改。
整数常量
-
整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,没有前缀则表示十进制。
-
整数常量也可以有后缀,可以是 U 和 L 的组合,其中,U 和 L 分别表示 unsigned 和 long。后缀可以是大写或者小写,多个后缀以任意顺序进行组合。
212 /* 合法 */
215u /* 合法 */
0xFeeL /* 合法 */
078 /* 非法:8 不是一个八进制数字 */
032UU /* 非法:不能重复后缀 */
85 /* 十进制 */
0213 /* 八进制 */
0x4b /* 十六进制 */
30 /* int */
30u /* 无符号 int */
30l /* long */
30ul /* 无符号 long */
浮点常量
-
一个浮点常量是由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。
3.14159 /* 合法 */ 314159E-5L /* 合法 */ 510E /* 非法:不完全指数 */ 210f /* 非法:没有小数或指数 */ .e55 /* 非法:缺少整数或小数 */
-
使用小数形式表示时,必须包含小数点、指数或同时包含两者。使用指数形式表示时,必须包含整数部分、小数部分或同时包含两者。有符号的指数是用 e 或 E 表示的。
字符常量
-
字符常量是括在单引号里,例如,‘x’,且可存储在一个简单的字符类型变量中。一个字符常量可以是一个普通字符(例如’x’)、一个转义序列(例如’\t’)或者一个通用字符(例如’\u02C0’)。
-
在 C# 中有一些特定的字符,当它们的前面带有反斜杠时有特殊的意义,可用于表示换行符(\n)或制表符 tab(\t)。
字符串常量
- 字符串常量是括在双引号
""
里,或者是括在@""
里。字符串常量包含的字符与字符常量相似,可以是:普通字符、转义序列和通用字符 - 使用字符串常量时,可以把一个很长的行拆成多个行,可以使用空格分隔各个部分。
string a = "hello, world"; // hello, world
string b = @"hello, world"; // hello, world
string c = "hello \t world"; // hello world
string d = @"hello \t world"; // hello \t world
string e = "Joe said \"Hello\" to me"; // Joe said "Hello" to me
string f = @"Joe said ""Hello"" to me"; // Joe said "Hello" to me
string g = "\\\\server\\share\\file.txt"; // \\server\share\file.txt
string h = @"\\server\share\file.txt"; // \\server\share\file.txt
常量定义
- 常量是使用 const 关键字来定义的 。定义一个常量的语法如下:
const <data_type> <constant_name> = value;
public const int c1 = 5;
public const int c2 = 6;
-
静态常量(编译时常量)const
在编译时就确定了值,必须在声明时就进行初始化且之后不能进行更改,可在类和方法中定义。定义方法如下:
const double a=3.14;// 正确声明常量的方法 const int b; // 错误,没有初始化
-
动态常量(运行时常量)readonly
在运行时确定值,只能在声明时或构造函数中初始化,只能在类中定义。定义方法如下:
class Program { readonly int a=1; // 声明时初始化 readonly int b; // 构造函数中初始化 Program() { b=2; } static void Main() { } }
-
静态常量与动态常量的使用场景
在下面两种情况下,可以使用 const 常量:
- 取值永久不变(比如圆周率、一天包含的小时数、地球的半径等)。
- 对程序性能要求非常苛刻。
除此之外的其他情况都应该优先采用 readonly 常量。
4.类型转换
隐式数值转换
#region 知识点一 相同大类型之间的转换
//有符号 long int short sbyte
long l = 1;
int i = 1;
short s = 1;
sbyte sb = 1;
//隐式转换 int隐式转换成了long
//可以用大范围 装小范围的 类型 (隐式转换)
l = i;
//不可能够用小范围的类型去装大范围的类型
//i = l;
//无符号 ulong uint ushort byte
ulong ul = 1;
uint ui = 1;
ushort us = 1;
byte b = 1;
ul = ui;
ul = us;
ul = b;
ui = us;
ui = b;
us = b;
//浮点数 decimal double——> float
decimal de = 1.1m;
double d = 1.1;
float f = 1.1f;
//decimal这个类型 没有办法用隐式转换的形式 去存储 double和float
//de = d;
//de = f;
//float 是可以隐式转换成 double
d = f;
//特殊类型 bool char string
//他们之间 不存在隐式转换
#endregion
#region 知识点二 不同大类型之间的转换
#region 无符号和有符号之间
//无符号 不能装负数
ulong ul2 = 1;
uint ui2 = 1;
ushort us2 = 1;
byte b2 = 1;
//无符号装有符号
//有符号的变量 是不能够 隐式转换成 无符号的
// b2=sb2;
//us2=sb2;
//ul2=sb2;
#endregion
#region 无符号装有符号
//有符号的变量 是不能够 隐式转换成 无符号的
// b2=sb2;
//us2=sb2;
//ul2=sb2;
#endregion
#region 有符号装无符号
//有符号的变量 是可以 装 无符号变量的 前提是 范围一定要是涵盖的 存在隐式转换
//i2 = ui2; 因为有符号的变量 可能会超过 这个无符号变量的范围;
//i2 = b2; 因为有符号的变量 不管是多少 都在 无符号变量的范围内
#endregion
#region 浮点数装整数 整数转为浮点数 是存在隐式转换的
//decimal de2 = 1.1m;
//double d2 = 1.1;
//float f2 = 1.1f;
#endregion
#region 浮点数 是可以装载任何类型的 整数的
//f2 = l2;
//f2 = ul2;
#endregion
//decimal 不能隐式存储 float和double 但是他可以隐式的存储整形
//double——> float——> 所有整形(无符号、有符号)
//decimal——> 所有整形(无符号、有符号)
//整数装浮点数 整数是不能隐式存储 浮点数 因为整数不能存储小数
#region 特殊类型和其他类型之间
//bool 没有办法和其他类型 相互隐式转换
//char 没有办法隐式的存储 其他类型的变量
//char类型 可以隐式转换成 整形和浮点型
//char隐式转换成 数据类型是
//对应的数字 其实是一个 ASCII码
//计算机里面存储 2进制
//字符 中文 英文 标点符号 在计算机中都是一个数字
//一个字符 对应一个数字 ACSII码就是一种对应关系
//string 类型 无法和其他类型进行隐式转换
#endregion
#endregion
#region 总结
//高精度(大范围)装低精度(小范围)
//double——> float——> 整数(无符号、有符号)——> char
//decimal——> 整数(无符号、有符号)——> char
//string 和 bool 不参与隐式转换规则的
显式数值转换
#region 知识点一 括号强转
//作用 一般情况下 将高精度的类型强制转换位低精度
//语法:变量类型 变量名 =(变量类型)变量;
//注意:精度问题 范围问题
//相同大类的整形
//有符号整形
sbyte sb = 1;
short s = 1;
int i = 1;
long l = 1;
//强转的时候 可能会出现范围问题 造成的异常
s = (short)i;
Console.WriteLine(s);
//无符号整形
byte b = 1;
ushort us = 1;
uint ui = 1;
ulong ul = 1;
b = (byte)ui;
Console.WriteLine(b);
//无符号和有符号
uint ui2 = 1;
int i2 = 1;
//在强转的时候一定要注意范围 不然得到的结果 可能有异常
ui2 = (uint)i2;
Console.WriteLine(ui2);
i2 = (int)ui2;
//浮点和整形 浮点数 强转成 整形时 会直接抛弃小数点后面的小数
i2 = (int)1.24f;
Console.WriteLine(i2);
//char和数值类型
i2 = 'A';
char c = (char)i2;
Console.WriteLine(c);
//bool和string 是不能通过 括号强转的
bool bo = true;
//int i3 = (bool)bo;
string str = "123";
//i3 = (int)str;
#endregion
#region 知识点二 Parse法
//作用 把字符串类型转换为对应的类型
//语法:变量类型.Parse("字符串");
//注意:字符串必须能够转换成对应类型 否则报错
//有符号
string str2 = "123";
int i4 = int.Parse("123");
Console.WriteLine(i4);
//我们填写字符串 必须是要能够转成对应类型的字符 如果不符合规则 会报错
//i4 = int.Parse("123.45");
//Console.WriteLine(i4);
//值的范围 必须是能够被变量存储的值 否则报错
short s3 = short.Parse("4000");
Console.WriteLine(s3);
sbyte sb3 = sbyte.Parse("1");
Console.WriteLine(s3);
//他们的意思是相同的
Console.WriteLine(sbyte.Parse("1"));
Console.WriteLine(long.Parse("123123"));
//无符号
Console.WriteLine(byte.Parse("1"));
Console.WriteLine(ushort.Parse("1"));
Console.WriteLine(uint.Parse("1"));
Console.WriteLine(ulong.Parse("1"));
//浮点数
float f3 = float.Parse("1.2323");
double d3 = double.Parse("1.2323");
//特殊类型
bool b5 = bool.Parse("true");
Console.WriteLine(b5);
char c2 = char.Parse("A");
Console.WriteLine(c);
#endregion
#region 知识点三 Convert法
//作用 更准确的将 各个类型之间进行相互转换
//语法:Convert.To目标类型(变量或常量)
//注意:填写的变量或常量必须正确 否则出错
//转字符串 如果是把字符串转对应类型 那字符串一定要合法合规
int a = Convert.ToInt32("12");
Console.WriteLine(a);
//精度更准确
//精度比括号强转好一点 会四舍五入
a = Convert.ToInt32(1.23456f);
Console.WriteLine(a);
//特殊类型转换
//把bool类型也可以转成 数值类型 true对应1 false对应0
a = Convert.ToInt32(true);
Console.WriteLine(a);
a = Convert.ToInt32(false);
Console.WriteLine(a);
a = Convert.ToChar("A");
Console.WriteLine(a);
//每一个类型都存在对应的 Convert中的方法
sbyte sb5 = Convert.ToSByte("1");
short s5 = Convert.ToInt16("1");
int i5 = Convert.ToInt32("1");
long l5 = Convert.ToInt64("1");
byte b6 = Convert.ToByte("1");
ushort us6 = Convert.ToUInt16("1");
uint ui6 = Convert.ToUInt32("1");
ulong ul6 = Convert.ToUInt64("1");
float f5 = Convert.ToSingle("12.3");
double d5 = Convert.ToDouble("13.2");
decimal de5 = Convert.ToDecimal("13.2");
bool bo5 = Convert.ToBoolean("true");
char c5 = Convert.ToChar("A");
string str5 = Convert.ToString("123123");
#endregion
#region 知识点四 其他类型转string
//作用 拼接打印
//语法:变量.Tostring();
string str6 = 1.ToString();
str6 = true.ToString();
str6 = "A".ToString();
str6 = 1.2f.ToString();
int aa = 1;
str6 = aa.ToString();
bool bo6 = true;
str6 = bo6.ToString();
//当我们进行字符串拼接时 就会自动调用 Tostring 转成 string
Console.WriteLine("123123" + 1 + true);
str6 = "123123" + 1 + true + 1.23;
#endregion
5.异常捕获
简介:
- 异常处理是指程序在运行过程中,发生错误会导致程序退出,这种错误,就叫做异常。
- 因此处理这种错误,就称为异常处理。
- 引起异常的原因,一般是使用者不正当操作,开发者没有按规范的处理数据、使用技术不当导致的,极少情况是由于.NET内部错误引起的。
使用:
C# 异常处理时建立在四个关键词之上的:try、catch、finally 和 throw。
- try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。
- catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。
- finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。
- throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。
try
{
<可能出现异常的代码>
}
catch(Exception e) 其中e为捕获到的异常,我们可以通过e了解到异常的具体信息。
{
<出现异常后执行的代码>
}
finally
{
<不管有没有异常都要执行的代码(可选)>
}
6.运算符
算数运算符
优先级:
一元的运算符的优先级要高于二元的运算符。
运算符 | 描述 |
---|---|
+ | 把两个操作数相加 |
- | 把第一个操作数中减去第二个操作数 |
* | 把两个操作数相乘 |
/ | 分子除以分母 |
% | 取模运算符,整除后的余数 |
++ | 自增运算符,整数值增加1 |
– | 自检运算符,整数值减少1 |
+= | 令运算的数等于它本身与等号后的数相加的值 |
-= | 令运算的数等于它本身与等号后的数相减的值 |
- a++ 先进行 运算 再 自增
- ++a 先进行 自增 再 运算
关系运算符
运算符 | 描述 |
---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 |
逻辑运算符
运算符 | 描述 | 推理 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | 有假即假 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | 有真即真 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | 真假相反 |
位运算符
运算符 | 描述 |
---|---|
& | 如果同时存在于两个操作数中,二进制AND运算符复制一位到结果中。 |
| | 如果存在于任一操作数中,二进制OR运算符复制一位到结果中。 |
^ | 如果存在于其中一个操作数中但不同时存在于两个操作数中,二进制异或运算 符复制一位到结果中。 |
~ | 按位取反运算符是一元运算符,具有翻转"位效果,即0变成1,1变成0,包括 符号位。 |
<< | 二进制左移运算符。左操作数的值向左移动右操作数指定的位数。 |
>> | 二进制右移运算符。左操作数的值向右移动右操作数指定的位数。 |
赋值运算符
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C=A+B 将把 A+B的值赋给C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C+=A 相当于 C=C+A |
-= | 减目赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C-=A 相当于 C=C-A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C*=A 相当于 C=C*A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C/=A 相当于 C=C/A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C%=A 相当于 C=C%A |
<<= | 左移且赋值运算符 | C<<=2 等同于 C=C<<2 |
>>= | 右移且赋值运算符 | C>>=2 等同于 C=C>>2 |
&= | 按位与目赋值运算符 | C&=2 等同于 C=C&2 |
^= | 按位异或目赋值运算符 | C^=2 等同于 C=C^2 |
|= | 按位或且赋值运算符 | C | =2 等同于 C=C | 2 |
三元运算符
运算符 | 描述 | 实例 |
---|---|---|
? : | 条件表达式 | 如果条件为真 ? 则为 X : 否则为 Y |
其他运算符
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回数据类型的大小 | sizeof(int); 将返回4 |
typeof() | 返回数据的类型 | sizeof(4); 将返回int |
& | 返回变量的地址 | &a; 将得到变量的实际地址 |
* | 变量的指针 | a*; 将指向一个变量。 |
is | 判断对象是否为某一类型 | if(Ford is Car) ; 检查Ford是否是Car类的一个对象。 |
as | 强制转换,即使转换失败也不会抛出异常 | Object obj = new StringReader(“Hello”); StringReader r = obj as StringReader; |
运算符重载
概念
-
让自定义类和结构体 能够使用运算符
-
使用关键字
operator
-
不可重载
的运算符:$$ || 索引[] 强转() 特殊运算符 点. 三目运算符?: 赋值符号= -
特点
- 一定是一个公共的静态方法
- 返回值写在operatori前
- 逻辑处理自定义
-
作用
- 让自定义类和结构体对像可以进行运算
-
注意
- 条件运算符需要成对实现
- 一个符号可以多个重载
- 不能使用ref和out
//实例 class Point { public int x; public int y; //重载 + 运算符 public static Point operator +(point p1,point p2) { Point p = new Point(); p.x = p1.x + p2.x; p.y = p1.y + p1.y; } } //使用 Point p1 = new Point(); p1.x = 1; p1.y = 1; Point p2 new Point(); p2.x = 2; p2.y = 2; Point p3 = p1 + p2;
7.判断及循环语句
- break关键词
- 跳出本 层 循环(通常与if连用)
- continue关键词
- 结束 本次 循环(continue后面的代码不再执行),进入下次循环。(通常与if连用)
判断语句
-
条件运算符(三元运算符)
- 条件表达式 ? 结果a : 结果b
-
if 的第一种形式
if(条件表达式){ 语句1; }
-
if 的第二种形式
if (条件表达式) { 语句1; } else { 语句2; }
-
if 的第三种形式
if (条件表达式1) { 语句1; } else if (条件表达式2) { 语句2; } else { 语句3; }
-
switch 选择
-
如果 case 冒号后面没有任何语句,可以不加 break;
-
switch()括号中是可以允许添加浮点型变量的,但
不推荐
-
浮点型是有误差的
-
浮点型一般不做等于的判断
- 企业面试题:有一个浮点数,判断该浮点数是不是等于5
-
switch(变量) { // 变量 == 常量 执行 case 和 break 之间的代码 case 常量: //满足某些条件时做的事情是一样的就可以使用贯穿 case 常量: //不写case后面配对的break就叫做贯穿 case 常量: //满足1342其中一个条件就会执行之后的代五 //满足条件执行的代码逻辑 break; case 常量: //满足条件执行的代码逻辑 break; case 可以有无数个 default: //可以忽略不写 如果上面case的条件都不满足 就会执行 default 中的代码 break; } //常量!!只能写一个值 不能去写一个范围 不能写条件运算符啊 逻辑运算符啊
-
循环语句
-
while循环
//while循环是先判断条件再执行 while (条件表达式) { //循环内容 }
-
do while
//do while循环是 先斩后奏 先至少执行一次 循环语句块中的逻辑再判断是否继续 do { //循环内容 }while(bool类型的值);
-
for循环
//for循环 for(参数初始化; 条件判断; 更新循环变量){ 循环操作; } //例: List<Person> people = new List<Person>(); for(int i = 0; i < 100; i++){ var p = people[i]; }
-
foreach
foreach
是为可迭代的对象(iteratable)专门设计的,能够只遍历一次的情况下,完成对有元素的访问。- foreach 语句经常与数组一起使用,在 C# 语言中提供了
foreach
语句遍历数组中的元素 - C#中的
for
和foreach
的设计目的是不一样的,for
是一般性的循环,而foreach
是专门用于可以迭代的集合的循环方法,能够有效地减少访问次数,从而达到优化的效果。
foreach(数据类型 变量名 in 数组名) { //语句块; } //例子: List<Person> people = new List<Person>(); foreach(var p in people) }
二.C#基础 (复杂数据类型)
1. 数据类型
在 C# 中,变量分为以下几种类型:
-
值类型(Value types)
-
引用类型(Reference types)
-
指针类型(Pointer types)
1.1 值类型
- 它变我不变-存储在栈内存。
- 值类型变量声明后,不管是否已经赋值,编译器为其分配内存。
- 引用类型当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。
- 值类型的实例通常是在线程栈上分配的(静态分配),但是在某些情形下可以存储在堆中。
- 引用类型的对象总是在进程堆中分配(动态分配)
整数类型
sbyte 有符号数,占用1个字节,-27一27-1
byte 无符号数,占用1个字节,0一28-1
short 有符号数,占用2个字节,-215一215-1
ushort 无符号数,占用2个字节,0一216-1
int 有符号数,占用4个字节,-231一231-1
uint 无符号数,占用4个字节,0一232-1
long 有符号数,占用8个字节,-263一263-1
ulong 无符号数,占用8个字节,0一264-1
浮点型
float 单精度浮点型,占用4个字节,最多保留7位小数
double 双精度浮点型,占用8个字节,最多保留16位小数
举例如下: double d = 1234.45; float f = 1234.45f;
- C#中还有一种精度更高的浮点类型:decimal类型,它占16个字节,要把数字指定为decimal类型,可以在数字的后面加上字符M或(m) 举例如下: decimal d=12.30M;
-
字符型
char
-
字符型只能存放一个字符,它固定占用两个字节,能存放一个汉字。 字符型用 char 关键字表示,存放到 char 类型的字符需要使用单引号括起来,例如 ‘a’、‘中’ 等。 举例如下: char c = ‘A’;
-
注意:字符型只能使用单引号。 双引号代表字符串类型。
-
-
布尔类型
bool
- C# 语言中,布尔类型使用 bool 来声明,它只有两个值,即 true 和 false。
枚举
-
枚举是一个比较特别的存在,它是一个被命名的整形常量的集合,一般用它来表示
动作状态、类型
等 -
枚举是直接在
命名空间、类或结构
中使用 enum 关键字定义的。所有常量名都可以在大括号内声明,并用逗号分隔。 -
枚举声明的位置通常是:
命名空间的下面, 类的外面
, 表示这个命名空间下, 所有的 类都可以访问这个枚举 -
枚举使用
enum
关键字来声明,与类同级,枚举本身可以有修饰符,但枚举的成员始终是公开的,不能有访问修饰符,枚举本身的修饰符仅能使用Public
和internal
。 -
枚举类型的枚举成员均为静态,且默认值为Int32类型
-
每个枚举成员均具有相关联的常数值,此值的类型就是枚举的底层数据类型,每个枚举成员的常数值必须在该枚举的底层数据类型的范围之内,如果没有明确指定底层数据类型则默认的数据类型是int类型。
-
枚举类型不能相同,但枚举的值可以相同,也就是枚举前边的符号不能相同,但是后边的值就可以相同
-
枚举是隐式密封的,不允许作为基类派生子类
-
申明枚举和申明枚举变量是两个概念
申明枚举: 相当于是创健一个自定义的枚举类型
申明枚举变量: 使用申明的自定义枚举类型创健一个枚举变量//枚举名 以E或者E_开头 作为我们的命名规范 enum E_自定义枚举名 { 自定义枚举项名字, //0 枚举中包裹的整形常量第一个默认值是0 下面会 *依次累加* 自定义枚举项名字1,//1 可以给枚举赋值 下面会 *依次累加* 自定义枚举项名字2,//2 } //声明枚举 enum E_PlayerType { Main, other } //申明枚举变量 //自定义的枚举类型 变量名 = 默认值;(自定义的枚举类型,枚举项) E_PlayerType playerType = E_PlayerType.Main; if(playerType == E_PlayerType.Main) { Console.WriteLine("主玩家逻辑"); }else if(playerType == E_PlayerType.Other) { Console.WriteLine("其它玩家逻辑"); } //枚举和switch是天生一对 E_PlayerType playerType = E_PlayerType.Main; switch (playerType) { case E_PlayerType.Main: Console.WriteLine("主玩家逻辑"); break; case E_PlayerType.Other: Console.WriteLine("其它玩家逻辑"); break; default: break; }
结构体
-
结构体是一种自定义变量类型
-
类似枚举需要自己定义
struct
-
它是数据和函数的集合 在结构体中可以申明名种变量和方法
-
作用:用来表现在关系的数据集合 比如用结构体表现学生,动物,人类等等
//结构体一般写在namespace语句块中 //结构体关键字 struct 自定义结构体名 //注意结构体名字 我们的规范是 帕斯卡命名法 { //第一部分 //变量 //第二部分 //构造函数(可选) //第三部分 //函数 } //实例:结构体类型的创建 //学生类型 struct Student { //变量 //结构体申明的变量不能直接初始化 //变量类型可以写 任意类型包括结构体 但是 不能是自己的结构体 public string name; public char sex; public int age; //构造函数(可选) //自定义构造函数 一般是用于在外面方便初始化 public Student(string name,char sex,int age) { //作用:快速给结构体字段赋初值 //而且必须给每一个字段都赋初值 //新的关键字 this 代表自己 下面即代表结构体里的变量 this.name = name; this.sex = sex; this.age = age; } //函数 //注意在结构体中的函数方法目前不需要加static关键字 void Speak() { //函数中可以直接使用结构体内部申明的变量 Console.WriteLine("我的名字是(o),我今年(l}岁",name,age); } //可以根据需求写无数个函数 }
-
结构体的构造函数 (可以重载)
- 结构体默认的构造函数,开发者不能创建默认构造(即无参构造),必须有参数
- 没有返回值,函数名必须和结构体名相同
- 如果申明了构造函数那么必须在其中对所有变量数据初始化
结构体的使用
//定义一个学生变量
Student st1;
//学生结构内变量赋值
st1.name = "xiaoming";
st1.age = 16;
st1.sex = 'M';
st1.Speak();
//使用构造函数初始化
Student s2 = new Student("xiaohong","M",18);
1.2 引用类型
引用类型不包含存储在变量中的实际数据,但它们包含对变量的引用,存储实际数据的地址,存储在堆内存-它变我也变。
换句话说,它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置的引用类型有:object、dynamic 和 string。
- 从内存上看,值类型是在栈中的操作,而引用类型是在堆中的操作。
(导致 => 值类型存取速度快,引用类型存取速度慢。) - 从本质上看,值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针或引用。
(值类型是具体的那个数值所占用的空间大小,而引用类型是存放那个数值的空间地址。) - 从来源上看,值类型继承自System.ValueType,引用类型继承自System.Object。
- 特别的:结构体是值类型,类和string是引用类型。
字符串(String)类型 特殊
-
string
非常的特殊它具备值类型的特征-它变我不变,因为重新赋值时 会在堆中重新分配空间。 -
string
虽然方便但是有一个小缺点就是频繁的改变string
重新赋值会产生内存垃圾
. -
字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。
string str = "runoob.com";
-
字符串
本质是char数组
- 转为char数组
ToCharArray()
方法
string str="小明"; Console.WriteLine(str[0]); //转为char数组 char[]chars = str.ToCharArray();
- 转为char数组
-
字符串拼接
str = string.Format("{0}{1}",1,3333); //13333
-
正向查找字符位置
string str="小明"; int index=str.IndexOf("明"); //从前往后查找 index = 1 没有返回-1
-
反向查找字符位置
string str="小明"; int index=str.LastIndexOf("明"); //从后往前查找index = 1 没有返回-1
-
移除指定位置后的字符
string str = "小明大明大壮"; string s = str.Remove(2);//不会改原字符串 会返回新字符串"小明" 第二个位置及之后的都移除 //参数一 开始位置 参数二 字符个数 string s = str.Remove(2,2);//不会改原字符串 会返回新字符串"小明大壮"
-
替换指定字符串
string str = "小明大明大壮"; string s = str.Replace("大明","小小");//不会改原字符串 会返回新字符串"小明小小大壮"
-
大小写转换
string str = "rmb"; //小写转大写 string s = str.ToUpper();//不会改原字符串 会返回新字符串"RMB" //大写转小写 string t = s.ToLower();//不会改原字符串 会返回新字符串"rmb"
-
字符串截取
string str = "小明大明大壮"; //截取从指定位置开始之后的字符串 string s = str.Substring(2);//不会改原字符串 会返回新字符串"大明大壮" //参数一 开始位置 参数二 字符个数 //不会自动的帮助你判断是否越界你需要自己去判断 string s = str.Substring(2,2);//不会改原字符串 会返回新字符串"大明"
-
字符串切割
string str ="1,2,3,5,6,7,8"; string[] strs = str.Split(',');//通过逗号切割 放入数组
StringBuilder
-
修改字符串而不创建新的对象,需要频繁修改和拼接的字符串可以使用它,可以提升性能
-
使用前需要引用命名空间
//引用命名空间 才可使用StringBuilder using Systen.Text; //初始化 直接指明内容 StringBuilder str = new StringBuilder("123321"); //本质还是字符数组
-
容量
- StringBuilder存在一个容量的问题
- 初始化时会预留未使用空间,往里面增加时,超过空间容量会自动扩容(16,32,64)
- 获得容量
Console.WriteLine(str.Capacity);//查看容量
- 获得字符长度
Console.WriteLine(str.Length);
-
增删查改替换
//增加 str.Append("4444"); //1233214444 str.AppendFormat("{0}{1}",100,999); //1233214444100999 //插入 str.Insert(0,"小明"); //小明1233214444100999 //删除 str.Remove(0,7); //14444100999 //查 Console.WriteLine(str[1]); //4 //改 str[0] = 'A'; //A4444100999 //替换 str.Replace("1","9"); //改变原字符串 A9444100999 //清空 str.Clear(); //空 //判断StringBuilder是否和某一个字符串相等 if(str.Equals("123")) { }
数组
-
概念:同一变量类型的数据集合
-
一定要掌握的知识:申明,遍历,增删查改
-
所有的变量类型都可以申明为数组
-
它是用来批量存储游戏中的同一类型对象的 容器 比如所有的怪物,所有玩家。
-
一维数组
-
变量类型[ ] 数组名;/只是申明了一个数组但是并没有开辟空间
-
变量类型可以是我们学过的或者没学过的所有变量类型
-
变量类型[ ] 数组名=new 变量类型[ ]
//1.数组声明 int[] arr1; //变量类型[] 数组名 = new 变量类型[数组的长度]; int[] arr2 = new int[5];//这种方式相当于开了5个房间但是 房间里面的int值 默认为0 //变量类型[] 数组名 = new 变量类型[数组的长度]{内容1,内容2,内容3,........}; int[] arr3 = new int[5]1,2,3,4,5}; //变量类型[] 数组名 = new 变量类型{内容1,内容2,内容3,}; int[] arr4 = new int[]1,2,3,4,5};//后面的内容就决定了数组的长度 //变量类型【】数组名={内容,内容2,内容3,........}; int[] arr5 = {1,3,4,5,6}; //2.数组的长度 //数组变量名.Length arr3.Length; //3.获取数组中的元素 //数组中的下标和索引他们是从0开始的 //通过 索引下标 去获得数组中某一个元素的值时 //一定注意!!!!! 不能越界数组的房间号范围是 0~Length-1 arr3[0]; //求arr3数组下标为0的元素 //4.修改数组中的元素 arr3[0] = 99; //5.遍历数组通过循环快速获取数组中的每一个元素 for (int i = 0;i < arr3.Length; i++) { Console.WriteLine(arr3[i]); } //6.增加数组的元素 //数组初始化以后是不能够直接添加新的元素的 int[] arrnew = new int[6]; //搬家 for (int i=0;i < arr3.Length; i++) { arrnew[i] = arr3[i]; }
- 二维数组
-
二维数组是使用两个下标(索引)来确定元素的数组
-
两个下标可以理解成行标和列标,比如矩阵
//二维数组的声明 //变量类型[,] 二维数组变量名; int[,] arr;//申明过后会在后面进行初始化 //变量类型[,] 二维数组变量名 = new变量行[行,列]; int[,] arr2 = new int[3,3]; //变量类型[,] 二维数组变量名 = new 变量类型[行,列]{{0行内容1,0行内容2,0行内容3},{1行内容1,1行内容2,1行内容3}}; int[,] arr3 = new int[3,3] {{1,2,3}, {4,5,6}, {7,8,9}}; //变量类型[,] 二维数组变量名 = new 变量类型[,]{{0行内容1,0行内容2,0行内容3},{1行内容1,1行内容2,1行内容3}}; int[,] arr4 = new int[,] {{1,2,3}, {4,5,6}, {7,8,9}}; //变量类型[,] 二维数组变量名 = {{0行内容1,0行内容2,0行内容3},{1行内容1,1行内容2,1行内容3}}; int[,] arr5 = {{1,2,3}, {4,5,6}, {7,8,9}};
-
二维数组的使用
//1.二维数组的长度 //我们要获取行和列分别是多长 //总长度 arr5.Length; //得到多少行 arr5.GetLength(0); //得到多少列 arr5.GetLength(1); //2.二维数组的元素访问 arr5[2,2]; //3.修改二维数组中的元素 arr5[2,2] = 99; //4.二维数组的遍历 for (int i = 0; i < arr5.GetLength(0); i++) { for (int j = 0; j < arr5.GetLength(1); j++) { Console.Write(arr5[i,j] + "\t"); } //换行 Console.WriteLine(); } //foreach迭代遍历 //foreach循环,这种循环遍历数组和集合更加简洁。 //foreach性能消耗要大一点,所以能用for的尽量用for //使用foreach循环遍历数组时,无须获得数组和集合长度,无须根据索引来访问数组元素,foreach循环自动遍历数组和集合的每一个元素。 //注意:迭代遍历是只读的,不能修改 foreach (var item in arr5) { Console.WriteLine(item); //迭代遍历是只读的,不能写入 }
- 交错数组
-
交错数组是元素为数组的数组。交错数组元素的维度和大小可以不同。交错数组有时称为“数组的数组”
//1.数组的声明 //变量类型[][] 交错数组名; int[][] arr1; //变量类型[][] 交错数组名 = new 变量类型[行数][]; int[][] arr2 = new int[3][]; //变量类型[][] 交错数组名 = new变量类型[行数][]{一维数组1,一维数组2,......}; int[][] arr3 = new int[3][]{new int[]{1,2,3}, new int[]{1,2} new int[]{1}}}; //变量类型[][] 交错数组名 = new变量类型[][]{一维数组1,一维数组2,......}; int[][] arr4 = new int[][]{new int[]{1,2,3}, new int[]{1,2} new int[]{1}}}; //变量类型[][] 交错数组名 = {一维数组1,一维数组2,......}; int[][] arr5 = {new int[]{1,2,3}, new int[]{1,2} new int[]{1}}}; //2.数组的长度 //行 arr5.GetLength(0)); //得到某一行的列数 arr5[0].Length; //3.获取交错数组中的元素 //注意:不要越界 arr5[0][1]; //4.修改交错数组中的元素 arr5[0][1]=99; //5.遍历交错数组 for (int i = 0; i < arr5.GetLength(0); i++) { for (int j = 0; j < arr5[0].Length; j++) { Console.Write(arr5[i][j] + "\t"); } //换行 Console.WriteLine(); }
类和对象
基本概念
-
具有相同特征,具有相同行为,一类事物的抽象
-
类是对象的模板,可以通过类创建出对象,类的关键词
class
-
类一般申明在namespace语句块中
//类的声明 //命名:用帕斯卡命名法 //注意:同一个语句块中的不同类不能重名 class 类名 { //特征一成员变量 //行为一成员方法 //保护特征一成员属性 //构造函数和析构函数 //索器 //运算符重载 //静态成员 }
成员变量
基本规则
-
申明在类语句块中
-
用来描述对象的特征
-
可以是任意变量类型
-
数量不做限制
-
是否赋值根据需求来定
//枚举 enum E_SexType { } //结构体 struct Position { } //类 class Pet { } class Person { //特征一 成员变量 可以是任意变量类型 //姓名 string name="唐老狮"; //年龄 public int age; //性别 public E_SexType sex; //女朋友 *//如果要在类中申明一个和自己 相同类型 的成员变量时* //不能对它进行实例化 会进入死循环 内存溢出卡死 public Person gridFriend; //朋友 public Person[] Friends; //位置 public Position pos; //宠物 private Pet pet = new Pet(); }
访问修饰符
-
public 一 公共的 自已(内部)和别人(外部)都能访问和使用
-
private 一 私有的 自己(内部)才能防问和使用 不写默 认为private
-
protected 一 保护的 自己(内部)和子类才能访问和使用
-
目前决定类内部的成员 的 访问权限
- 成员变量的使用和初始值
- 值类型 来说数字类型 默认值都是
0
bool 类型false
- 引用类型的
null
- 看默认值的小技巧 default(int)
成员方法
基本概念
成员方法(函数用来表现对象行为)
-
申明在类语句块中
-
是用来描述对象的行为的
-
规则和函数申明规则相同
-
受到访问修饰符规则影响
-
返回值参数不做限制
-
方法数量不做限制
-
注意:
- 成员方法
不要加static
关键字 - 成员方法 必须实例化出对象 再通过对象来使用 相当于该对象执行了某个行为
- 成员方法 受到访问修饰符影响
class Person { public string name; public int age; //不带访问修饰符 默认为私有private bool IsAdult() { return age >= 18; } //可以使用成员变量 public void Speak(string str) { IsAdult(); //私有的内部才能使用的方法 Console.WriteLine("{0}说{1}",name,str) } } //使用 Person p = new Person p.name = "小明”; p.age = 18 p.Speak("你好"); if(p.IsAdult()) { p.Speak("我18岁了"); }
- 成员方法
成员属性
-
用于保护成员变量
-
为成员属性的获取和赋值添加逻辑处理
-
解决的局限性
- public一内外访问
- private一内部访问
- protected一内部和子类访问
- 属性
可以让
成员变量在外部
只能获取 不能修改 或者 只能修改不能获取
class Person { private string name; private int age; private int money; private bool sex; //朋友 public Person[] Friends; //属性的命名一般使用帕斯卡命名法 //注意: //1.默认不加会使用属性申明时的访问权限 //2.加的访问修饰符要低于属性的访问权限 //3.不能让get和set的访问权限都低于属性的权限 public string Name { get //可以+private { //可以在返回之前添加一些逻辑规则 取出来 解密 //意味着 这个属性可以获取的内容 return name; } set { //可以在设置之前添加一些逻辑规则 存进去 加密 //value关键字 用于表示 外部传入的值 name = value; } } //自动属性 //作用:外部能得不能改的特征 //如果类中有一个特征是只希望外部能得不能改的又没什么特殊处理 //那么可以直接使用自动属性 public int age { //没有再get和set中写逻辑的需求或者想法 get; set; } } //使用 Person p = new Person(); p.Name = "小明"; //执行set Person p2 = new Person(); p2.name ="大明"; p2.ag = 16; //添加到p的Person[] Friends里 p.AddFriend(p2);
索引器
基本概念
-
让对象可以像数组一样通过索引访问其中元素,使程序看起来更直观,更容易编写
-
比较适用于在
类中有数组变量
时使用可以方便的访问和进行逻辑处理//访问修饰符 返回值 this[参数类型 参数名,参数类型 参数名......] { //内部的写法和规则和成员属性相同 get{}; set{}; } //实例 class Person { private string name; private int age; private int[,] arr; private Person[] friends; //索引器重载 public int this[int i,int j] { get { return arr[i,j]; } set { arr[i,j] = value; } } //索引器重载 public string this[string str] { get { switch(str) { case "name": return this.name; case "age": return age.ToString(); } return ""; } } //索引器 public Person this[int index] { get { //可以写逻辑的根据需求来处理这里面的内容 //不为空,不越界则返回 否则返回null 不写这个若数组为空 越界 则会报错 if(friends == null||friends.Length - 1 < index) { return null; } return friends[index]; } set { //value代表传入的值 //可以写逻辑的根据需求来处理这里面的内容 if(friends == null) { friends = new Person[]{ value }; } else if(idnex > friends.Length - 1) { //自己定了一个规则 如果索引越界 就默认把最后一个朋友顶掉 friends[friends.Length - 1] = value; } friends[index] = value; } } } //索引器的使用 Person p = new Person(); P[0] = new Person(); //访问Person this[int index]的 set Console.WriteLine(p[0]); //访问Person this[int index]的 get 得到
静态成员
-
关键词
static
-
静态成员
- 成员:字段、属性、方法
- 静态:跟对象没有任何关系,
只跟类有关系
-
静态成员在何时开辟的内存
- 程序开始运行时就会分配内存空间,所以我们就能直接使用
-
静态成员在何时释放内存
- 在程序结束的时候才会释放
-
普通的实例成员,每有一个对象,就有一个该成员
- 而静态成员,跟对象没有关系,所以无论有多少个对象,静态成员都只有一个
- 例: 实例成员【name】,每有一个人,就会有对应的一个名字
- 而静态成员【Population】,跟对象没有关系,无论有多少个实例对象,人口数量只有一个
-
静态方法中是不可以访问非静态的成员的
- 不能访问非静态的字段、属性
- 不能调用非静态的方法
-
非静态方法中是可以访问静态成员的
- 能访问静态的字段、属性
- 能调用静态的方法
-
静态方法是可以有重载
//自定义静态成员 class Test { //静态变量 public static float PI = 3.1415926f; //普通成员变量 public int testInt = 100; //静态方法 public static float Calccircle(float r) { //静态函数中不能使用非静态成员 //r = PI*testInt; return PI * r * r; } //普通成员方法 public int TestFun(int x) { //非静态函数可以使用静态成员 生命周期原因 return x*PI; } } //静态成员的使用 //因为 静态成员 的生命周期原因 可以 不实例化 直接点出静态方法成员 Test.Calccircle(2.0f); Console.WriteLine(Test.PI); // 普通成员变量 只能将对象 实例化 出来后才能点出来使用 不能无中生有 Test t = new Test(); Console.WriteLine(t.TestFun(2));
静态类
特点
-
静态类中
只能存在静态成员
,不能存在非静态的成员
-
静态类是
不能进行实例化
的 -
作用:
- 将常用的静态成员写在静态类中方便使用
- 静态类不能被实例化,更能体现工具类的唯一性
- 比如Console就是一个静态类
static class Tools { //静态成员变量 public static int testIndex = 0; public static void TeseFun() { } public static int TestIndex { get; set; } }
-
静态构造函数
- 只有一种写法
- static 类名()
- 静态构造函数必须无参数
- 静态构造函数在什么时候才会调用
- 静态构造函数在程序运行期间
只会执行一次
- 在第一次访问该类的时候调用
- 用这个类去new一个对象
- 用这个类去访问某个静态成员
- 用这个类去调用某个静态方法
- 静态构造函数在程序运行期间
- 如果有继承关系
- 静态构造函数的执行顺序是:
- 先执行子类的静态构造,再执行父类的静态构造
- 先子后父
- 静态构造有什么作用
- 一般用于对静态成员进行初始化
static class Tools { //静态成员变量 public static int testIndex = 0; //静态构造函数 不管在不在静态类 只会自动调用一次 //作用:初始化静态成员 static Tools() { } //普通构造 new的时候就会调用 public Tools() { } } //用这个类去访问某个静态成员 静态方法 会调用静态构造函数 只会调用一次 初始化静态成员 Tools.testIndex;
- 只有一种写法
-
拓展方法
概念
- 为现有非静态变量类型添加新方法
- 作用
- 提升程序拓展性
- 不需要再对象中重新写方法
- 不需要继承来添加方法
- 为别人封装的类型写额外的方法
- 特点
- 一定是
写在静态类中
- 一定是个
静态函数
- 第一个参数为拓展目标
- 第一个参数用this修饰
- 一定是
static class Tools { //为int拓展了一个成员方法 //成员方法 是需要 实力化对象后 才 能使用的 //value 代表 使用该方法的 实例化对象 public static void NewValue(this int value,int r) { //拓展的方法 的逻辑 } //为Person类拓展了一个成员方法 Person类为非静态类 静态类不能为静态类拓展方法 //拓展方法和原有的方法名重复了 用的会是原方法 public static void NewPerson(this Person value) { //拓展的方法 的逻辑 } } //拓展方法的使用 int i = 10; //第一个参数为本身的值 //第一个参数后的参数才会作为这个函数的参数 i.NewValue(20); //在此处NewValue(this int value)的value 代表 i的值 即10
类对象
-
基本概念
- 类的申明 和类对象(变量)申明是两个概念
- 类的申明 类似 枚举 和 结构体的申明 类的申明相当于申明了一个自定义变量类型
- 而对象 是类创建出来的
- 相当于申明一个指定类的变量
- 类创建对象的过程 一般称为实例化对像
- 类对象 都是引用类型的
//对象基本语法 //类名 变量名; //类名 变量名 = null; (null代表空) //类名 变量名 = new 类名(); //实例: class Person { //特征一成员变量 //行为一成员方法 //保护特征一成员属性 //构造函数和析构函数 //索器 //运算符重载 //静态成员 } //实例化对象 Person p; Person P2 = nu11; //null代表空不分配堆内存空间 Person p3 = new Person(); //相当于一个人对象 Person p4 = new Person(); //相当于又是一个人对象 //注意 //虽然他们是来自一个类的实例化对像 //但是他们的 特征 行为等等信息 都是他们 独有 的 //千万干万 不要觉得他们是共享了数据 两个人 你是你 我是我 彼此没有关系
密封类
基本概念
-
密封类是使用
sealed
密封关键字修饰的类 -
作用:
-
在面向对象程序的设计中,密封类的主要作用就是不允许最底层子类被继承
-
可以保证程序的规范性、安全性
-
-
意义:加强面向对象程序设计的规范性、结构性、安全性
//让Father类无法被继承 sealed class Father { }
1.3 指针型
指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。
声明指针类型的语法:
type* identifier;
char* cptr;
int* iptr;
2. 函数(方法)
-
本质是一块具有名称的代码块
-
可以使用函数(方法)的名称来执行该代码块
-
函数(方法)是封装代码进行重复使用的一种机制
-
函数(方法)的主要作用
- 封装代码
- 提升代码复用率(少写点代码)
- 抽象行为
-
函数写在哪里
- cass语句块中
- structi语句块中
//static 返回类型 函数名(参数类型参数名1,参数类型参数名2,···)
{
//函数的代码逻辑;
//函数的代码逻辑;
//......
return 返回值; //如果有返回类型才返回
}
//1.关于static 不是必须的
//2-1.关于返回类型 引出一个新的关键字 void(表示设有返回值)
//2-2.返回类型可以写任意的变量类型 14种变量类型 + 复杂数据类型(数组、枚举、结构体、类class)
//3.关于函数名 使用帕斯卡命名法命名 myName(驼蜂命名法)MyName(帕斯卡命名法)
//4-1.参数不是必须的,可以有0~n个参数 参数的类型也是可以是任意类型的 14种变量类型+复杂数据类型(数组、枚举、结构体、类c1ass) 多个参数的时候需要用逗号隔开
//4-2.参数名 驼峰命名法
//5.当返回值类型不为void时 必须通过新的关键词 return 返回对应类型的内容(注意:即使是void也可以选择性使用return)
-
有参有多返回值函数
static int[]Calc(int a,int b) { int sum a +b; int avg sum 2; int[] arr = {sum,avg }; return arr; } //或者 static int[]Calc(int a,int b) { int sum = a + b; int avg = sum / 2; return new int[] { sum,avg }; }
-
关于return
- 即使函数没有返回值,我们也可以使用return
- return可以直接不执行之后的代码,直接返回到函数外部
引用参数ref
-
添加了ref关键词的参数
- 传递的就不是值了
- 而是
地址
- 而如果没有赋初值,是没有地址的
- 所以ref参数
一定是个变量
- 所以ref参数的
实参一定是赋过初值
-
所以ref一般加在
值类型
参数的前面 -
使用应用参数,
无论是形参还是实参
前面都要加ref
关键词//未加ref 相当于让函数内的value = 外部传过来的x(值类型),原本外部x值是不会变的 static void ChangeArrayValue(ref int value) { value = 99; } static void Main(string[] args) { //ref传入的变量必须在外部赋值out不用 int x = 2; ChangeArrayValue(ref x); Console.WriteLine(x); //结果输出99 //它们可以解决 在函数内部改变外部传入的内容 里面变了 外面也要变 }
输出参数out
-
添加了out关键词的参数
- 参数就成了一个输出的通道
离开方法之前形参必须赋值
- 实参
必须是一个变量
- 传递的实参一般是
值类型
-
使用输出参数,
无论是形参还是实参
前面都要加out
关键词//未加ref 相当于让函数内的value = 外部传过来的x(值类型),原本外部x值是不会变的 static void ChangeArrayValue(out int value) { //out传入的变量必须在内部赋值ref不用 value = 99; } static void Main(string[] args) { int x = 2; ChangeArrayValue(out x); Console.WriteLine(x); //结果输出99 //它们可以解决 在函数内部改变外部传入的内容 里面变了 外面也要变 }
变长参数 params 和参数默认值
-
变长参数关键字
params
-
params int[] 意味着可以传入n个int参数 n可以等于0 传入的参数会存在arr数组中
-
注意:
- params关键字后面必为
数组
- 数组的类型可以是任意的类型
- 函数参数可以有 别的参数和params关键字修饰的参数
- 函数参数中只能最多出现一个params关键字 并且一定是在
最后一组参数
前面可以有n个其它参数
- params关键字后面必为
//实例
static int Sum(params int[] arr)
{
int sum = 0;
for (int i=0;i<arr.Length;i++)
{
sum += arr[i];
}
return sum;
}
//使用
Sum();
Sum(1,2,3,4,5,6,7,8,1);
参数默认值
-
有参数默认值的参数一般称为可选参数
-
作用是当调用函数时可以不传入参数,不传就会使用默认值作为参数的值
-
注意:
- 支持多参数默认值每个参数都可以有默认值
- 如果要混用 可选参数 必须写在 普通参数后面
static void Speak(string test, string str="我设什么话可说") { Console.WriteLine(str); }
函数重载
重载概念
-
在同一语句块
class
或者struct
中 -
函数(方法)名相同
参数的数量不同
-
参数的数量相同,但
参数的类型
或顺序不同
-
作用:
- 命名一组功能相似的函数,减少函数名的数量,避免命名空间的污染
- 提升程序可读性
-
注意:
- 重载 和返回值类型 无关,只和
参数类型
,个数
,顺序
有关 - 调用时程序会自己根据传入的参数类型判断使用哪一个重载
//实例 static int CalcSum(int a,int b) { return a + b; } //参数数量不同 static int CalcSum(int a,int b,int c) { return a + b + c; } //数量相同,类型不同 static float Calcsum(int a,float b) { return a + b; } //使用 调用时程序会自己根据传入的参数类型判断使用哪一个重载 CalcSum(1,2); CalcSum(1.2,3); Ca1csum(1,2.3f);
- 重载 和返回值类型 无关,只和
递归函数
-
方法自己调用自己
-
多个方法之间来回调用
-
使用递归时
一定要有出口
-
使用递归时,一定概要慎重慎重再慎重…
//实例 求x的阶层 x! int Fun(int x) { if(x == 0) return 1; else return x*Fun(x-1); }
构造函数
基本概念
-
在实例化对象时会调用的用于初始化的函数
-
构造函数没有返回值
- 也不能写返回值类型
-
如果不写 默认 存在一个无参构造函数
-
构造函数的写法
- 没有返回值
- 函数名和类名必须相同
- 没有特殊需求时一般都是public的
- 构造函数可以被重载
this
代表当前 调用该函数的对象 自己
-
注意:
- 如果不自己实现无参构造函数而实现了有参构造函数
- 会失去默认的无参构造
//实例 class Person { public string name; public int age; //无参构造 //类中是允许自己申明无参构造函数的 //结构体是不允许 public Person() { name = “小明”; age = 18; } //有参构造 public Person(int age) { //this代表当前调用该函数的对象自己 this.age = age; } //构造函数可以被重载 public Person(string name,int age) { //this代表当前调用该函数的对象自己 this.name = name; this.age = age; } } //构造函数的调用 //实例化对象,调用无参构造 Person p1 = new Person(); //实例化对象,调用有参构造 Person p2 = new Person("唐老狮",18);
-
构造函数特殊写法
- 可以通过
this
重用构造函数代码
//函数后面this(),表示先调用无参构造函数,再调用本函数 public Person(string name,int age):this() { this.name = name; this.age = age; } Person p new = Person("小明",18); //会先到this(),即先调用无参构造函数 //函数后面this(int age),表示先调用有参构造函数Person(int age),再调用本函数 //会先把传进来的age参数,传给有参构造函数Person(int age); public Person(string name,int age):this(age) { this.name = name; this.age = age; } Person p new = Person("小明",18); //会先到this(age),即先调用有参构造函数
- 可以通过
析构函数
-
当引用类型的堆内存被回收时,会调用该函数
-
对于需要手动管理内存的语言(比如C++),需要在析构函数中做一些内存回收处理
-
但是C#中存在自动垃圾回收机制GC
-
所以我们几乎不会怎么使用析构函数。除非你想任素一个对象被垃圾回收时,做一些特殊处理
-
注意:在Unity开发中析构函数几乎不会使用,所以该知识点只做了解即可
//当引用类型的堆内存被回收时 //析构函数 是当垃圾 真正被回收的时候才会调用的函数 ~Person() { }
垃圾回收机制
-
垃圾回收,英文简写Gc (Garbage Collector)
-
垃圾回收的过程是在遍历堆(Heap)上动态分配的所有对象
-
通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用
-
所谓的垃圾就是没有被任何变量,对象引用的内容
-
垃圾就需要被回收释放
-
垃圾回收有很多种算法,比如
- 引用计数(Reference Counting)
- 标记清除(Mark Sweep)
- 标记整理(Mark Compact)
- 复制集合(Copy Collection)
-
注意:
-
Gc只负责堆(Heap)内存的垃圾回收
-
引用类型
都是存在堆(Heap)中的,所以它的分配和释放都通过垃圾回收机制来管理 -
栈(Stack)上的内存是由系统自动管理的
-
值类型
在栈(Stck)中分配内存的,他们有自己的申明周期,不用
对他们进行管理,会自动分配和释放
-
-
C#中内存回收机制的大概原理
-
0代内存 1代内存 2代内存
-
代的概念:
-
代是垃圾回收机制使用的一种算法(分代算法)
-
新分配的对象都会被配置在第0代内存中
-
每次分配都可能会进行垃圾回收以释放内存(0代内存满时)
-
-
在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步
-
标记对像从根(静态字段、方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象
不可达对象就认为是垃圾 -
搬迁对象压缩堆 (挂起执行托管代码线程) 释放末标记的对象 搬迁可达对象 修改引用地址
-
-
大对象总被认为是第二代内存 目的是减少性能损耗,提高性能
-
不会对大对象进行搬迁压缩 85088字节(83kb) 以上的对象为大对象
手动触发垃圾回收(Loding场景加载)
- 手动触发垃圾回收的方法
- 一般情况下我们不会频繁调用
- 都是在Loadingi过场景时才调用
GC.Collect();
3. 继承
基本概念
-
一个类A继承一个类B
-
类A将会继承类B的所有成员
-
A类将拥有B类的所有特征和行为
-
被继承的类 称为 父类、基类、超类
-
继承的类称为 子类、派生类
-
子类可以有自己的特征和行为
-
特点
- 单根性 子类只能有一个父类
- 传递性子类可以间接继承父类的父类
class Teacher { //姓名 public string name; //职工号 public int number; //介绍名字 public void SpeakName() { Console.WriteLine(name); } } //继承Teacher类 class TeachingTeacher:Teacher { //具备Teacher类 特征和行为,并添加自己的特征和行为 //科目 public string subject; public new string name; //表示覆盖父类的name 默认也覆盖 写new为了规范化 public void SpeakSubject() { Console.WriteLine(subject+"老师"); } } //使用 TeachingTeacher th = new TeachingTeacher(); th.name = "小明"; th.number = 1; th.subject = "unity"; th.SpeakName(); th.SpeakSubject();
里式替换准则
基本概念
-
里氏替换原则是面向对象七大原则中最重要的原则
-
概念:
- 任何父类出现的地方,子类都可以替代
-
重点:
- 语法表现一父类容器装子类对象,因为子类对象包含了父类的所有内容
-
作用:
- 方便进行对象储和管理
//基本实现 class Gameobject { } class Player:Gameobject { public void PlayerAtk() { Console.WriteLine("玩家攻击"); } } class Monster:Gameobject { public void MonsterAtk() { Console.WriteLine("怪物攻击"); } } //使用 //里式替换准则 用父类容器 装载子类对象 Gameobject player = new Player(); Gameobject monster = new Monster(); Gameobject[] objects = new Gameobject[] {new Player(),new Monster()}; //用父类容器 装载子类对象 通过父类使用子类独特方法 //is 和 as
is 和 as
基本概念
-
is:判断对象是否是执行类对象
- 返回值:bool 是为真 不是为假
-
as:将一个对象转换为指定类对象
-
返回值:指定类型对像
-
成功返回执行类型对象,失败返回null
-
-
基本语法
- 类对象 is 类名 该语句块 会有一个bool返回值 true 和 false
- 类对象 as 类名 该语句块 会有一个对象返回值 对象 和 null
//is:判断一个对象是否是指定类对象 //返回值:boo1是为真不是为假 if(player is Player) { }else if(player is Monster) { } //as:将一个对象转换为指定类对象 //返回值:指定类型对像 //成功返回指定类型对象,失败返回null Player p = player as Player; //若失败 p = null; if(player is Player) { Player p = player as Player; p.PlayerAtk(); //一步到位 (player as Player).PlayerAtk(); }
继承中的构造函数
继承中构造函数 基本概念
-
特点
- 当申明一个子类对象时
- 先执行父类的构造函数
- 再执行子类的构造函数
-
注意:
- 父类的无参构造很重要 当子类实例化 会
默认调用父类无参构造
- 子类可以通过
base
关键字 代表父类 调用父类构造
- 父类的无参构造很重要 当子类实例化 会
-
继承中构造函数的执行顺序
- 父类的父类的构造 一 >。。。父类构造 一> 。。。一>子类构造
//实例 class Father { //当子类实例化 会默认调用父类无参构造 public Father() { Console.WriteLine("Father无参构造"); } public Father(int i) { Console.WriteLine("Father有参构造"); } } class Son:Father { //base和this相似 base关键字代表父类 this关键字代表自己 //通过base调用指定父类有参构造 解决若父类没有无参构造 而子类实例化报错问题 public Son(int i):base(i) { Console.WriteLine("Son有参1"); } public Son(int i,int y):this(i) { Console.WriteLine("Son有参2"); } } //使用 Son s = new = Son(1,2); //先到Son(int i,int y):this(i) 有this(i) 则到Son(int i):base(i) //有base 则到父类有参构造Father(int i) //所以输出为: Father有参构造 Son有参1 Son有参2
Object 所有类基类
万物之父
-
关键字:
object
-
概念:
object
是所有类型的基类 它是一个类(引用类型)
-
作用:
- 可以利用里氏替换原则,用object容器装所有对像
- 可以用来表示不确定类型,作为函数参数类型
//使用 //引用类型 object o = new Son(); if(o is son) { (o as Son).Speak(); } //值类型 object o2 = 1f; //用强转使用 float f = (float)o2; //特殊的string类型 object str = "123123"; string str2 = str.Tostring(); string str2 = str as string;
Object中的方法
object 中的静态方法:
-
静态方法
Equals
判断两个对象是否相等- 最终的判断权,交给左侧对像的Equals方法
- 不管值类型引用类型都会按照左侧对象Equls方法的规则来进行比较
class test { } //值类型 object.Equals(1,1); //true //引用类型 比的是地址是否相同 Test t1 = new Test(); Test t2 = new Test(); object.Equals(t1,t2); //false
-
静态方法
ReferenceEquals
- 比较两个对象是否是相同的引用,主要是用来比较引用类型的对象
- 值类型对象返回值始终是false
//值类型 object.ReferenceEquals(1,1); //false object.ReferenceEquals(t1,t2); //true
object中的成员方法:
-
普通方法
GetType
- 该方法在反射相关知识点中是非常重要的方法,之后我们会具体的讲解这里返回的Type类型。
- 该方法的主要作用就是获取对象运行时的类型Type
- 通过Type结合反射相关知识点可以做很多关于对象的操作。
Test t = new Test(); Type type = t.GetType();
-
普通方法
Memberwiseclone
- 该方法用于获取对象的浅拷贝对象,口语化的意思就是会返回一个新的对象,但是新对象中的引用变量会和老对象中一致。
class Test { public int i = 1; public Test Clone() { //返回一个克隆类 因为Memberwiseclone是保护类型的 无法在外部调用 所以要内部返回 return Memberwiseclone() as Test; } } //使用 Test t = new Test(); public t = t.Clone();
object中的虚方法:
-
虚方法
Equals
- 默认实现还是比较两者是否为同一个引用,即相当于ReferenceEquals.
- 但是微软在所有值类型的基类System.ValueType中重写了该方法(上面的Equals),用来比较值相等。
- 我们也可以重写该方法,定义自己的比较相等的规则
-
虚方法
GetHashCode
- 该方法是获取对象的哈希码
- (一种通过算法算出的,表示对象的唯一编码,不同对象哈希码有可能一样,具体值根据
- 我们可以通过重写该函数来自己定义对像的哈希码算法,正常情况下,我们使用的极少
-
虚方法ToString
- 该方法用于返回当前对象代表的字符串,我们可以重写它定义我们自己的对象转字符串规则
- 该方法非常常用。当我们调用打印方法时,默认使用的就是对像的ToString,方法后打印出来的内容。
装箱拆箱
-
发生条件
- 用object存值类型(装箱)
- 再把object转为值类型(拆箱)
-
装箱
- 把值类型用引用类型存储
- 栈内存会迁移到堆内存中
-
拆箱
- 把引用类型存储的值类型取出来
- 堆内存会迁移到栈内存中
-
好处:不确定类型时可以方便参数的存储和传递
-
坏处:存在内存迁移,增加性能消耗
//装箱 object v = 3; //拆箱 int intValue = (int); public void Fun(params boject[] arr) { } Fun(1,2.0f,34.5,"123",new Son());
4. 多态
多态按字面的意思就是“多种状态”
-
让继承同一父类的子类们在执行相同方法时有不同的表现(状态)
-
主要目的:
- 同一父类的对像执行相同行为(方法)有不同的表现
-
解决的问题
- 让同一个对像有唯一行为的特征
//解决的问题 同一个对象执行不同方法的问题 Father f new Son(); f.SpeakName(); (f as Son).SpeakName();
-
如果父类想要子类可以重写该函数
- 那么父类的该函数必须是一个虚函数
- [访问修饰符] virtual 返回值类型 函数名(参数列表)
-
子类该怎么重写
- [访问修饰符] override 返回值类型 函数名(参数列表)
class GameObject { //虚函数virtual 用来给子类重写 public virtual void Atk() { Console.WriteLine("游戏对象进行攻击); } } class Player:GamgeObject { //override重写虚函数 public override void Atk() { //base的作用 //代表父类可以通过bas来保留父类的行为 base.Atk(); Console.WriteLine("玩家对象进行攻击); } } //使用 Object p = new Player(); //都是调用子类的Atk方法 p.Atk(); (p as Player).Atk();
抽象类
概念
-
被抽象关键字abstract修饰的类
-
特点:
- 不能被实例化的类
- 可以包含抽象方法
- 继承抽象类必须重写其抽象方法
//实例 //抽象不能被实例化 abstract class Thing { //抽象类中封装的所有知识点都可以在其中书写 public string name; //可以在抽象类中写抽象函数 }
抽象方法(函数)
-
用
abstract
关键字修饰的方法 -
特点:
- 只能在抽象类中申明
- 没有方法体
- 不能是私有的
- 继承后必须实现 用
override
重写
-
什么时候使用:
- 父类中的行为不太需要被实现的,只希望子类去定义具体的规则的可以选择抽象类然后使用其中的抽象方法来定义规则(定义怪物类,各种怪物继承后必须实现不同抽象方法,相当于实现不同战斗方式)
//实例 abstract class Fruits { public string name; //抽象方法 是不能有函数体的 //不能是私有的 //继承后必须实现 用override重写 public abstract void Bad(); } class Apple:Fruits { //实现父类抽象方法 不然报错 //虚方法和抽象方法 都可以被子类无限重写 public override void Bad() { } }
5. 单例、接口和范型
单例
单例模式是比较常见的一种设计模式,目的是保证一个类只能有一个实例,而且自行实例化并向整个系统提供这个实例,避免频繁创建对象,节约内存。
例如,界面上只能有一个鼠标指针,并且该鼠标指针能被所有程序访问。同样的还有,企业解决方案可以与管理到特定系统连接的单网关对象进行对接。
- 如果一个对象在声明时直接实例化【new】。
- 在访问这个类的时候调用
- 实例化的时间点最早,在静态构造之前就执行了
饿汉式
- 这是比较常见的写法,在类加载的时候就完成了实例化,避免了多线程的同步问题。当然缺点也是有的,因为类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,内存就浪费了。
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
普通的懒汉式 (线程不安全,不可用)
- 这是懒汉式中最简单的一种写法,只有在方法第一次被访问时才会实例化,达到了懒加载的效果。但是这种写法有个致命的问题,就是多线程的安全问题。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果,所以这种写法不可采用。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
同步方法的懒汉式 (可用)
- 这种写法是对getInstance()加了锁的处理,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,为了改进这种写法,就有了下面的双重检查懒汉式。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类 (可用,推荐)
-
这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。
-
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
枚举 (可用、推荐) java
- 线程安全问题。因为Java虚拟机在加载枚举类的时候会使用ClassLoader的方法,这个方法使用了同步代码块来保证线程安全。
- 避免反序列化破坏对象,因为枚举的反序列化并不通过反射实现
public class Singleton {
private Singleton(){
}
public static enum SingletonEnum {
SINGLETON;
private Singleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
}
}
在代码中,我们首先将Singleton类的构造函数设置为private私有的,然后在Singleton类中定义一个静态的枚举类型SingletonEnum。
在SingletonEnum中定义了枚举类型的实例对象Singleton,再按照单例模式的要求在其中定义一个Singleton类型的对象instance,其初始值为null;我们需要将SingletonEnum的构造函数改为私有的,在私有构造函数中创建一个Singleton的实例对象;最后在getInstance()方法中返回该对象。
接口
基本概念
-
接口是行为的抽象规范
-
它也是一种自定义类型
-
关键字
interface
-
接口申明的规范
- 不包含成员变量
- 只包含方法、属性、索引器、事件
- 成员不能被实现
- 成员可以不用写访问修饰符,不能是私有的
- 接口不能继承类,但是可以继承另一个接口
-
接口的使用规范
- 类可以继承多个接口
- 类继承接口后,必须实现接口中所有成员
-
使用时机
:把行为抽象出去,让需要的类去继承(同一种类型,不同行为或不同类型,同种行为用接口) -
特点:
- 它和类的申明类似
- 接口是用来继承的
- 接口不能被实例化,但是可以作为容器存储对象
//1.接口的声明 接口关键字:interface //一句话记忆:接口是抽象行为的基类” //接口命名规范帕斯卡前面加个I interface IFly { //1.不包含成员变量 //2.只包含方法、属性、索引器、事件 //成员不能是私有的 不写默认公共的 类继承接口后,必须实现接口中所有成员 void Fly(); //自动属性 string Name { get; set; } //索引器 int this[int index] { get; set; } //事件 event Action doSomthing; } //接口的使用 class Animal { } //类可以继承1个类,n个接口 //继承了接口后 必须实现其中的内容 并且必须是public的 class Person:Animal,Fly { //接口也遵循里氏替换原则 //可以与virtual 虚方法配合使用 让子类重写 public virtual void Fly() { } public string name { get; set; } public int this[int index] { get; { return 0; } set { } } public event Action doSomthing; } //2.接口继承接口时不需要实现 //待类继承接口后类自己去实现所有内容 //注意:显示实现接口时不能写访问修饰符 interface ISuperAtk { void Atk(); } interface IAtk { void Atk(); } class Player IAtk,ISuperAtk { //显示实现接口就是用接口名.行为名去实现 void IAtk.Atk() { } void ISuperAtk.Atk() { } public void Atk() { } } //显示实现接口 使用 IAtk ia = new Player(); ISuperAtk isa = new Player(); Player() p = new Player(); ia.Atk(); isa.Atk(); p.Atk();
泛型
- 泛型实现了类型参数化,达到代码重用目的
- 通过类型参数化来实现同一份代码上操作多种类型
- 申明泛型时 泛型相当于类型占位符
- 定义类或方法时使用替代符代表变量类型
当真正使用
类或者方法时再具体指定类型
泛型类
-
class 类名<泛型占位字母>
- 类中的字段类型、方法参数、方法返回值,都可以使用类中的泛型
class Test<T> { public T value; } class Testclass<T,T1,t2> //T相当于占位符 实现了类型参数化 { public T value1; public T1 value2; public T3 value3; } //使用 //<int,string>告诉了T类型为int,T1类型String TestClass<int,string,Test<int>> t = new TestClass<int,string,Test<int>>(); t.value1 = 10; //Testclass<T,T1>中的变量value1为int类型 赋值为10 t2.value2 = "123123"; //Testclass<T,T1>中的变量value2为string类型 赋值为"123123"
泛型接口
-
interface 接口名<泛型占位字母>
interface TestInterface<T> { T Value { get; set; } } //继承 继承要填类型 class Test:TestInterface<int> { //接口实现 public int Value { get; set; } }
-
泛型方法
//普通类中的泛型方法 class Test { public void TestFun<T>(T value) { } //重载 public void TestFun<T>() { //用泛型类型 在里面做一些逻辑处理 T t = default(T); //default()得到类型默认值 因为T类型未知 } //重载 T作为返回值 public T TestFun<T>(string v) { return default(T); } //多个返回值 public T TestFun<T,K,M>(T t,K k,M m) { } } //使用 Test t = new Test1(); t.TestFun<string>("123123");
//泛型类中的泛型方法 class Test<T> //与上面的class Test 不是同一个类了 虽然名字一样 但是加了泛型 { public T value; //这个不叫泛型方法因为T是泛型类申明的时候就指定在使用这个函数的时候 //我们不能再去动态的变化了 public void TestFun<T t> { Console.WriteLine(t); } //这个叫泛型方法 public void TestFun<K k> { Console.WriteLine(k); } } //使用 函数参数可以变化的才叫 泛型方法 Test<int> t = new Test<int>(); t.TestFun<string>("123"); t.TestFun<float>(1.2f);
泛型的作用
-
不同类型对象的相同逻辑处理就可以选择泛型
-
使用泛型可以一定程度避免装箱拆箱
-
举例:优化ArrayList
//不同类型对象 相同逻辑处理 不同类型的增删查改 定义一次就行 class ArrayList<T> { private T[] arr; public void Add(T value) { } public void Remove(T value) { } }
泛型约束
-
关键字
where
-
让泛型的类型有一定的限制
-
泛型约束一共有6种
- 值类型 where 泛型字母:struct
- 引用类型 where 泛型字母:class
- 存在无参公共构造函数 where 泛型字母:new()
- 某个类本身或者其派生类 where 泛型字母:类名
- 某个接口的派生类型 where 泛型字母:接口名
- 另一个泛型类型本身或者派生类型 where 泛型字母:另一个泛型字母
//1.值类型约束 class Test<T> where T:struct //给占位符约束 值类型 { public T value; public void TestFun<K>(K k) where K:struct //给占位符约束 值类型 { } } //使用 Test<int> t = new Test<int>(); //占位符必须为 值类型 t.TestFun<float>(2.0f);
//2.引用类型约束 class Test<T> where T:class //给占位符约束 引用类型 { public T value; public void TestFun<K>(K k) where K:class //给占位符约束 引用类型 { } } //使用 Test<object> t = new Test<object>(); //占位符必须为 引用类型 t.TestFun<object>(new object);
//3.引用类型约束 class Test<T> where T:class //给占位符约束 引用类型 { public T value; public void TestFun<K>(K k) where K:class //给占位符约束 引用类型 { } } //使用 Test<object> t = new Test<object>(); //占位符必须为 引用类型 t.TestFun<object>(new object);
//4.无参公共构造约束 class Test<T> where T:new() { public T value; public void TestFun<K>(K k) where K:new() //给占位符约束 无参公共构造 { } } class Test1 { public void Test() //必须是公共的不然使用 无参公共构造约束 会报错 { } } //使用 Test<Test1> t = new Test<Test1>(); //有公共的无参构造类或结构体 抽象类不行 t.<Test1>(new Test1); Test<int> t1 = new Test<int>(); //值类型也行 Test<object> t2 = new Test<object>(); //引用类型也行
//5.类约束 class Test<T> where T:Test1 //给占位符约束 类约束 Test1为类名 { public T value; public void TestFun<K>(K k) where K:Test1 //给占位符约束 类约束 Test1为类名 { } } //使用 Test<Test1> t = new Test<Test1>(); //占位符必须为 约束的类名Test1 或其派生类(子类) t.TestFun<Test1>(new Test1);
//6.接口约束 //定义接口 interface IFly { } //继承接口 class Test1:IFly { } class Test<T> where T:IFly //给占位符约束 接口约束 IFly为接口名 某个接口派生类型 { public T value; public void TestFun<K>(K k) where K:IFly //给占位符约束 接口约束 IFly为接口名 { } } //使用 Test<IFly> t = new Test<IFly>(); t = new Test1(); //或者 其接口派生类型(接口子类) Test<Test1> t = new Test<Test1>(); t = new Test1();
//7.另一个泛型约束 interface IFly { } //继承接口 class Test1:IFly { } class Test<T,U> where T:U //给占位符约束 约束为:T和U一样或者T为U的派生类 { public T value; public void TestFun<K,V>(K k) where K:V //给占位符约束 U为另一个泛型约束 { } } //使用 Test<Test1,IFly> t = new Test<Test1,IFly>(); //Test1为IFly派生类
//约束的组合使用 class Test<T> where T:class,new() //约束为引用类型且有公共无参类型 { } //多个泛型有约束 T 和 K分别约束 class Test8<T,K> where T:class,new() where K:struct { }
密封方法
基本概念 用密封关键字sealed
修饰的重写函数
-
作用:让虚方法或者抽象方法之后
不能再被重写
-
特点:和
override
一起出现//实例 class Person { public override void Eat() { } }
6. 命名空间
概念
-
命名空间是用来组织和重用代码的
-
关键字
namespace
-
作用
- 就像是一个工具包,类就像是一件一件的工具,都是申明在命名空间中的
-
使用
- 不同命名空间中相互使用需要引用命名空间或指明出处
不同
命名空间中允许有同名类
- 命名空间可以包裹命名空间
//实例 //引用命名空间 using myGame using myGame.UI namespace myGame { //类 class Gameobject { } //命名空间可以包裹命名空间 namespace UI { class Image { } } } namespace na2 { //类 class Program { static void Main(string[] args); //不同命名空间中相互使用 需要引用命名空间或指明出处 Gameobject g = new Gameobject(); Image i = new Image(); //上面没有引用 或者引用的有同名应该指明出处 myGame.Gameobject g = new myGame.Gameobject(); myGame.UI.Image i = new myGame.UI.Image(); } }
-
修饰类的访问修饰符
- public – 公共的
- internal – 只能在该程序集中使用 命名空间中的类默认为
internal
- abstract – 抽象类
- sealed – 密封类
- partial – 分部类
7. 区别
结构体和类的区别
区别概述
-
结构体和类最大的区别是在存储空间上的,因为结构体是值,类是引用
-
因此他们的存储位置一个在栈上,一个在堆上
-
通过之前知调点的学习,我相信你能够从此处看出他们在使用的区别一值和引用对象在赋值时的区别
-
结构体和类在使用上很类似,结构体甚至可以用面向对象的思想来形容一类对象。
-
结构体具备着面向对象思想中封装的特性,但是它不具备继承和多态的特性,因此大大减少了它的使用频率
-
由于结构体不具备继承的特性,所以它不能够使用protected保护访问修饰符
细节区别
- 结构体是值类型,类是引用类型
- 结构体存在栈中,类存在堆中
- 结构体成员不能使用orotectedi访问修饰符,而类可以
- 结构体成员变量申明不能指定初始值,而类可以
- 结构体不能申明无参的构造函数,而类可以
- 结构体申明有参构造函数后,无参构造不会被顶掉
- 结构体不能申明析构函数,而类可以
- 结构体不能被继承,而类可以
- 结构体需要在构造函数中初始化所有成员变量,而类随意
- 结构体不能被静态
static
修饰(不存在静态结构体),而类可以 - 结构体不能在自己内部申明和自已一样的结构体变量,而类可以
结构体的特别之处
- 结构体可以继承接口因为接口是行为的抽象
如何选择结构体和类
- 想要用
继承和多态时
,直接淘汰结构体,比如玩家、怪物等等
- 对象是数据集合时,优先考虑结构体,比如
位置、坐标等等
- 从值类型和引用类型赋值时的区别上去考虑,比如
经常被赋值传递的对象
,并且
改变赋值对象,原对象不想跟着变化时,就用结构体。比如坐标、向量、旋转等等
抽象类和接口的区别
相同点
- 都可以被继承
- 都不能直接实例化
- 都可以包含方法申明
- 子类必须实现未实现的方法
- 都遵循里氏替换原则
区别
- 抽象类中可以有构造函数:接口中不能
- 抽象类只能被单一继承:接口可以被继承多个
- 抽象类中可以有成员变量;接口中不能
- 象类中可以申明成员方法,虚方法,抽象方法,静态方法;接口中只能申明没有实现的抽象方法
- 抽象类方法可以使用访问修饰符:接口中建议不写,默认public
如何选择抽象类和接口
- 表示对像的用抽象类,表示行为拓展的用接口
- 不同对象拥有的共同行为,我们往往可以使用接口来实现
- 举个例子:
- 动物是一类对像,我们自然会选择抽类;而飞翔是一个行为,我们自然会选择接口。
三.C#进阶
1. 简单数据结构类
ArrayList
ArrayList的本质
-
ArrayList:是一个C#为我们封装好的类
-
它的本质是一个object类型的数组
-
ArrayListi类帮助我们实现了很多方法
-
比如数组的增删查改
//声明
2. 常用泛型数据结构类
3. 委托和事件# Unity C#
简介
**C#**是微软公司发布的一种由C和C++衍生出来的面向对象的编程语言,它不仅去掉了 C++ 和 Java 语言中的一些复杂特性,还提供了可视化工具,能够高效地编写程序。
**C#**是由C和C++衍生出来的一种安全的、稳定的、简单的、优雅的面向对象编程语言。它在继承C和C++强大功能的同时去掉了一些它们的复杂特性(例如没有宏以及不允许多重继承)。
**C#**使得C++程序员可以高效的开发程序,且因可调用由 C/C++ 编写的本机原生函数,而绝不损失C/C++原有的强大的功能。因为这种继承关系,C#与C/C++具有极大的相似性,熟悉类似语言的开发者可以很快的转向C#。
一.C#入门
1.输入输出
//读取用户的输出,返回一个int类型
Console.Read();
//读取用户的输入,返回一个string类型
Console.ReadLine();
//输出数据
Console.Write("Hello world");
//输出数据并换行
Console.WriteLine("Hello world");
//读取用户输入,多用于暂停程序
Console.ReadKey();
2.变量
C# 中变量定义的语法:
int i, j, k;
char c, ch;
float f, salary;
double d;
变量定义时进行初始化:
int i = 100;
3.常量
- 常量是固定值,程序执行期间不会改变。常量可以是任何基本数据类型,比如整数常量、浮点常量、字符常量或者字符串常量,还有枚举常量。
- 常量可以被当作常规的变量,只是它们的值在定义后不能被修改。
整数常量
-
整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,没有前缀则表示十进制。
-
整数常量也可以有后缀,可以是 U 和 L 的组合,其中,U 和 L 分别表示 unsigned 和 long。后缀可以是大写或者小写,多个后缀以任意顺序进行组合。
212 /* 合法 */
215u /* 合法 */
0xFeeL /* 合法 */
078 /* 非法:8 不是一个八进制数字 */
032UU /* 非法:不能重复后缀 */
85 /* 十进制 */
0213 /* 八进制 */
0x4b /* 十六进制 */
30 /* int */
30u /* 无符号 int */
30l /* long */
30ul /* 无符号 long */
浮点常量
-
一个浮点常量是由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。
3.14159 /* 合法 */ 314159E-5L /* 合法 */ 510E /* 非法:不完全指数 */ 210f /* 非法:没有小数或指数 */ .e55 /* 非法:缺少整数或小数 */
-
使用小数形式表示时,必须包含小数点、指数或同时包含两者。使用指数形式表示时,必须包含整数部分、小数部分或同时包含两者。有符号的指数是用 e 或 E 表示的。
字符常量
-
字符常量是括在单引号里,例如,‘x’,且可存储在一个简单的字符类型变量中。一个字符常量可以是一个普通字符(例如’x’)、一个转义序列(例如’\t’)或者一个通用字符(例如’\u02C0’)。
-
在 C# 中有一些特定的字符,当它们的前面带有反斜杠时有特殊的意义,可用于表示换行符(\n)或制表符 tab(\t)。
字符串常量
- 字符串常量是括在双引号
""
里,或者是括在@""
里。字符串常量包含的字符与字符常量相似,可以是:普通字符、转义序列和通用字符 - 使用字符串常量时,可以把一个很长的行拆成多个行,可以使用空格分隔各个部分。
string a = "hello, world"; // hello, world
string b = @"hello, world"; // hello, world
string c = "hello \t world"; // hello world
string d = @"hello \t world"; // hello \t world
string e = "Joe said \"Hello\" to me"; // Joe said "Hello" to me
string f = @"Joe said ""Hello"" to me"; // Joe said "Hello" to me
string g = "\\\\server\\share\\file.txt"; // \\server\share\file.txt
string h = @"\\server\share\file.txt"; // \\server\share\file.txt
常量定义
- 常量是使用 const 关键字来定义的 。定义一个常量的语法如下:
const <data_type> <constant_name> = value;
public const int c1 = 5;
public const int c2 = 6;
-
静态常量(编译时常量)const
在编译时就确定了值,必须在声明时就进行初始化且之后不能进行更改,可在类和方法中定义。定义方法如下:
const double a=3.14;// 正确声明常量的方法 const int b; // 错误,没有初始化
-
动态常量(运行时常量)readonly
在运行时确定值,只能在声明时或构造函数中初始化,只能在类中定义。定义方法如下:
class Program { readonly int a=1; // 声明时初始化 readonly int b; // 构造函数中初始化 Program() { b=2; } static void Main() { } }
-
静态常量与动态常量的使用场景
在下面两种情况下,可以使用 const 常量:
- 取值永久不变(比如圆周率、一天包含的小时数、地球的半径等)。
- 对程序性能要求非常苛刻。
除此之外的其他情况都应该优先采用 readonly 常量。
4.类型转换
隐式数值转换
#region 知识点一 相同大类型之间的转换
//有符号 long int short sbyte
long l = 1;
int i = 1;
short s = 1;
sbyte sb = 1;
//隐式转换 int隐式转换成了long
//可以用大范围 装小范围的 类型 (隐式转换)
l = i;
//不可能够用小范围的类型去装大范围的类型
//i = l;
//无符号 ulong uint ushort byte
ulong ul = 1;
uint ui = 1;
ushort us = 1;
byte b = 1;
ul = ui;
ul = us;
ul = b;
ui = us;
ui = b;
us = b;
//浮点数 decimal double——> float
decimal de = 1.1m;
double d = 1.1;
float f = 1.1f;
//decimal这个类型 没有办法用隐式转换的形式 去存储 double和float
//de = d;
//de = f;
//float 是可以隐式转换成 double
d = f;
//特殊类型 bool char string
//他们之间 不存在隐式转换
#endregion
#region 知识点二 不同大类型之间的转换
#region 无符号和有符号之间
//无符号 不能装负数
ulong ul2 = 1;
uint ui2 = 1;
ushort us2 = 1;
byte b2 = 1;
//无符号装有符号
//有符号的变量 是不能够 隐式转换成 无符号的
// b2=sb2;
//us2=sb2;
//ul2=sb2;
#endregion
#region 无符号装有符号
//有符号的变量 是不能够 隐式转换成 无符号的
// b2=sb2;
//us2=sb2;
//ul2=sb2;
#endregion
#region 有符号装无符号
//有符号的变量 是可以 装 无符号变量的 前提是 范围一定要是涵盖的 存在隐式转换
//i2 = ui2; 因为有符号的变量 可能会超过 这个无符号变量的范围;
//i2 = b2; 因为有符号的变量 不管是多少 都在 无符号变量的范围内
#endregion
#region 浮点数装整数 整数转为浮点数 是存在隐式转换的
//decimal de2 = 1.1m;
//double d2 = 1.1;
//float f2 = 1.1f;
#endregion
#region 浮点数 是可以装载任何类型的 整数的
//f2 = l2;
//f2 = ul2;
#endregion
//decimal 不能隐式存储 float和double 但是他可以隐式的存储整形
//double——> float——> 所有整形(无符号、有符号)
//decimal——> 所有整形(无符号、有符号)
//整数装浮点数 整数是不能隐式存储 浮点数 因为整数不能存储小数
#region 特殊类型和其他类型之间
//bool 没有办法和其他类型 相互隐式转换
//char 没有办法隐式的存储 其他类型的变量
//char类型 可以隐式转换成 整形和浮点型
//char隐式转换成 数据类型是
//对应的数字 其实是一个 ASCII码
//计算机里面存储 2进制
//字符 中文 英文 标点符号 在计算机中都是一个数字
//一个字符 对应一个数字 ACSII码就是一种对应关系
//string 类型 无法和其他类型进行隐式转换
#endregion
#endregion
#region 总结
//高精度(大范围)装低精度(小范围)
//double——> float——> 整数(无符号、有符号)——> char
//decimal——> 整数(无符号、有符号)——> char
//string 和 bool 不参与隐式转换规则的
显式数值转换
#region 知识点一 括号强转
//作用 一般情况下 将高精度的类型强制转换位低精度
//语法:变量类型 变量名 =(变量类型)变量;
//注意:精度问题 范围问题
//相同大类的整形
//有符号整形
sbyte sb = 1;
short s = 1;
int i = 1;
long l = 1;
//强转的时候 可能会出现范围问题 造成的异常
s = (short)i;
Console.WriteLine(s);
//无符号整形
byte b = 1;
ushort us = 1;
uint ui = 1;
ulong ul = 1;
b = (byte)ui;
Console.WriteLine(b);
//无符号和有符号
uint ui2 = 1;
int i2 = 1;
//在强转的时候一定要注意范围 不然得到的结果 可能有异常
ui2 = (uint)i2;
Console.WriteLine(ui2);
i2 = (int)ui2;
//浮点和整形 浮点数 强转成 整形时 会直接抛弃小数点后面的小数
i2 = (int)1.24f;
Console.WriteLine(i2);
//char和数值类型
i2 = 'A';
char c = (char)i2;
Console.WriteLine(c);
//bool和string 是不能通过 括号强转的
bool bo = true;
//int i3 = (bool)bo;
string str = "123";
//i3 = (int)str;
#endregion
#region 知识点二 Parse法
//作用 把字符串类型转换为对应的类型
//语法:变量类型.Parse("字符串");
//注意:字符串必须能够转换成对应类型 否则报错
//有符号
string str2 = "123";
int i4 = int.Parse("123");
Console.WriteLine(i4);
//我们填写字符串 必须是要能够转成对应类型的字符 如果不符合规则 会报错
//i4 = int.Parse("123.45");
//Console.WriteLine(i4);
//值的范围 必须是能够被变量存储的值 否则报错
short s3 = short.Parse("4000");
Console.WriteLine(s3);
sbyte sb3 = sbyte.Parse("1");
Console.WriteLine(s3);
//他们的意思是相同的
Console.WriteLine(sbyte.Parse("1"));
Console.WriteLine(long.Parse("123123"));
//无符号
Console.WriteLine(byte.Parse("1"));
Console.WriteLine(ushort.Parse("1"));
Console.WriteLine(uint.Parse("1"));
Console.WriteLine(ulong.Parse("1"));
//浮点数
float f3 = float.Parse("1.2323");
double d3 = double.Parse("1.2323");
//特殊类型
bool b5 = bool.Parse("true");
Console.WriteLine(b5);
char c2 = char.Parse("A");
Console.WriteLine(c);
#endregion
#region 知识点三 Convert法
//作用 更准确的将 各个类型之间进行相互转换
//语法:Convert.To目标类型(变量或常量)
//注意:填写的变量或常量必须正确 否则出错
//转字符串 如果是把字符串转对应类型 那字符串一定要合法合规
int a = Convert.ToInt32("12");
Console.WriteLine(a);
//精度更准确
//精度比括号强转好一点 会四舍五入
a = Convert.ToInt32(1.23456f);
Console.WriteLine(a);
//特殊类型转换
//把bool类型也可以转成 数值类型 true对应1 false对应0
a = Convert.ToInt32(true);
Console.WriteLine(a);
a = Convert.ToInt32(false);
Console.WriteLine(a);
a = Convert.ToChar("A");
Console.WriteLine(a);
//每一个类型都存在对应的 Convert中的方法
sbyte sb5 = Convert.ToSByte("1");
short s5 = Convert.ToInt16("1");
int i5 = Convert.ToInt32("1");
long l5 = Convert.ToInt64("1");
byte b6 = Convert.ToByte("1");
ushort us6 = Convert.ToUInt16("1");
uint ui6 = Convert.ToUInt32("1");
ulong ul6 = Convert.ToUInt64("1");
float f5 = Convert.ToSingle("12.3");
double d5 = Convert.ToDouble("13.2");
decimal de5 = Convert.ToDecimal("13.2");
bool bo5 = Convert.ToBoolean("true");
char c5 = Convert.ToChar("A");
string str5 = Convert.ToString("123123");
#endregion
#region 知识点四 其他类型转string
//作用 拼接打印
//语法:变量.Tostring();
string str6 = 1.ToString();
str6 = true.ToString();
str6 = "A".ToString();
str6 = 1.2f.ToString();
int aa = 1;
str6 = aa.ToString();
bool bo6 = true;
str6 = bo6.ToString();
//当我们进行字符串拼接时 就会自动调用 Tostring 转成 string
Console.WriteLine("123123" + 1 + true);
str6 = "123123" + 1 + true + 1.23;
#endregion
5.异常捕获
简介:
- 异常处理是指程序在运行过程中,发生错误会导致程序退出,这种错误,就叫做异常。
- 因此处理这种错误,就称为异常处理。
- 引起异常的原因,一般是使用者不正当操作,开发者没有按规范的处理数据、使用技术不当导致的,极少情况是由于.NET内部错误引起的。
使用:
C# 异常处理时建立在四个关键词之上的:try、catch、finally 和 throw。
- try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。
- catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。
- finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。
- throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。
try
{
<可能出现异常的代码>
}
catch(Exception e) 其中e为捕获到的异常,我们可以通过e了解到异常的具体信息。
{
<出现异常后执行的代码>
}
finally
{
<不管有没有异常都要执行的代码(可选)>
}
6.运算符
算数运算符
优先级:
一元的运算符的优先级要高于二元的运算符。
运算符 | 描述 |
---|---|
+ | 把两个操作数相加 |
- | 把第一个操作数中减去第二个操作数 |
* | 把两个操作数相乘 |
/ | 分子除以分母 |
% | 取模运算符,整除后的余数 |
++ | 自增运算符,整数值增加1 |
– | 自检运算符,整数值减少1 |
+= | 令运算的数等于它本身与等号后的数相加的值 |
-= | 令运算的数等于它本身与等号后的数相减的值 |
- a++ 先进行 运算 再 自增
- ++a 先进行 自增 再 运算
关系运算符
运算符 | 描述 |
---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 |
逻辑运算符
运算符 | 描述 | 推理 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | 有假即假 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | 有真即真 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | 真假相反 |
位运算符
运算符 | 描述 |
---|---|
& | 如果同时存在于两个操作数中,二进制AND运算符复制一位到结果中。 |
| | 如果存在于任一操作数中,二进制OR运算符复制一位到结果中。 |
^ | 如果存在于其中一个操作数中但不同时存在于两个操作数中,二进制异或运算 符复制一位到结果中。 |
~ | 按位取反运算符是一元运算符,具有翻转"位效果,即0变成1,1变成0,包括 符号位。 |
<< | 二进制左移运算符。左操作数的值向左移动右操作数指定的位数。 |
>> | 二进制右移运算符。左操作数的值向右移动右操作数指定的位数。 |
赋值运算符
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C=A+B 将把 A+B的值赋给C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C+=A 相当于 C=C+A |
-= | 减目赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C-=A 相当于 C=C-A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C*=A 相当于 C=C*A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C/=A 相当于 C=C/A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C%=A 相当于 C=C%A |
<<= | 左移且赋值运算符 | C<<=2 等同于 C=C<<2 |
>>= | 右移且赋值运算符 | C>>=2 等同于 C=C>>2 |
&= | 按位与目赋值运算符 | C&=2 等同于 C=C&2 |
^= | 按位异或目赋值运算符 | C^=2 等同于 C=C^2 |
|= | 按位或且赋值运算符 | C | =2 等同于 C=C | 2 |
三元运算符
运算符 | 描述 | 实例 |
---|---|---|
? : | 条件表达式 | 如果条件为真 ? 则为 X : 否则为 Y |
其他运算符
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回数据类型的大小 | sizeof(int); 将返回4 |
typeof() | 返回数据的类型 | sizeof(4); 将返回int |
& | 返回变量的地址 | &a; 将得到变量的实际地址 |
* | 变量的指针 | a*; 将指向一个变量。 |
is | 判断对象是否为某一类型 | if(Ford is Car) ; 检查Ford是否是Car类的一个对象。 |
as | 强制转换,即使转换失败也不会抛出异常 | Object obj = new StringReader(“Hello”); StringReader r = obj as StringReader; |
运算符重载
概念
-
让自定义类和结构体 能够使用运算符
-
使用关键字
operator
-
不可重载
的运算符:$$ || 索引[] 强转() 特殊运算符 点. 三目运算符?: 赋值符号= -
特点
- 一定是一个公共的静态方法
- 返回值写在operatori前
- 逻辑处理自定义
-
作用
- 让自定义类和结构体对像可以进行运算
-
注意
- 条件运算符需要成对实现
- 一个符号可以多个重载
- 不能使用ref和out
//实例 class Point { public int x; public int y; //重载 + 运算符 public static Point operator +(point p1,point p2) { Point p = new Point(); p.x = p1.x + p2.x; p.y = p1.y + p1.y; } } //使用 Point p1 = new Point(); p1.x = 1; p1.y = 1; Point p2 new Point(); p2.x = 2; p2.y = 2; Point p3 = p1 + p2;
7.判断及循环语句
- break关键词
- 跳出本 层 循环(通常与if连用)
- continue关键词
- 结束 本次 循环(continue后面的代码不再执行),进入下次循环。(通常与if连用)
判断语句
-
条件运算符(三元运算符)
- 条件表达式 ? 结果a : 结果b
-
if 的第一种形式
if(条件表达式){ 语句1; }
-
if 的第二种形式
if (条件表达式) { 语句1; } else { 语句2; }
-
if 的第三种形式
if (条件表达式1) { 语句1; } else if (条件表达式2) { 语句2; } else { 语句3; }
-
switch 选择
-
如果 case 冒号后面没有任何语句,可以不加 break;
-
switch()括号中是可以允许添加浮点型变量的,但
不推荐
-
浮点型是有误差的
-
浮点型一般不做等于的判断
- 企业面试题:有一个浮点数,判断该浮点数是不是等于5
-
switch(变量) { // 变量 == 常量 执行 case 和 break 之间的代码 case 常量: //满足某些条件时做的事情是一样的就可以使用贯穿 case 常量: //不写case后面配对的break就叫做贯穿 case 常量: //满足1342其中一个条件就会执行之后的代五 //满足条件执行的代码逻辑 break; case 常量: //满足条件执行的代码逻辑 break; case 可以有无数个 default: //可以忽略不写 如果上面case的条件都不满足 就会执行 default 中的代码 break; } //常量!!只能写一个值 不能去写一个范围 不能写条件运算符啊 逻辑运算符啊
-
循环语句
-
while循环
//while循环是先判断条件再执行 while (条件表达式) { //循环内容 }
-
do while
//do while循环是 先斩后奏 先至少执行一次 循环语句块中的逻辑再判断是否继续 do { //循环内容 }while(bool类型的值);
-
for循环
//for循环 for(参数初始化; 条件判断; 更新循环变量){ 循环操作; } //例: List<Person> people = new List<Person>(); for(int i = 0; i < 100; i++){ var p = people[i]; }
-
foreach
foreach
是为可迭代的对象(iteratable)专门设计的,能够只遍历一次的情况下,完成对有元素的访问。- foreach 语句经常与数组一起使用,在 C# 语言中提供了
foreach
语句遍历数组中的元素 - C#中的
for
和foreach
的设计目的是不一样的,for
是一般性的循环,而foreach
是专门用于可以迭代的集合的循环方法,能够有效地减少访问次数,从而达到优化的效果。
foreach(数据类型 变量名 in 数组名) { //语句块; } //例子: List<Person> people = new List<Person>(); foreach(var p in people) }
二.C#基础 (复杂数据类型)
1. 数据类型
在 C# 中,变量分为以下几种类型:
-
值类型(Value types)
-
引用类型(Reference types)
-
指针类型(Pointer types)
1.1 值类型
- 它变我不变-存储在栈内存。
- 值类型变量声明后,不管是否已经赋值,编译器为其分配内存。
- 引用类型当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。
- 值类型的实例通常是在线程栈上分配的(静态分配),但是在某些情形下可以存储在堆中。
- 引用类型的对象总是在进程堆中分配(动态分配)
整数类型
sbyte 有符号数,占用1个字节,-27一27-1
byte 无符号数,占用1个字节,0一28-1
short 有符号数,占用2个字节,-215一215-1
ushort 无符号数,占用2个字节,0一216-1
int 有符号数,占用4个字节,-231一231-1
uint 无符号数,占用4个字节,0一232-1
long 有符号数,占用8个字节,-263一263-1
ulong 无符号数,占用8个字节,0一264-1
浮点型
float 单精度浮点型,占用4个字节,最多保留7位小数
double 双精度浮点型,占用8个字节,最多保留16位小数
举例如下: double d = 1234.45; float f = 1234.45f;
- C#中还有一种精度更高的浮点类型:decimal类型,它占16个字节,要把数字指定为decimal类型,可以在数字的后面加上字符M或(m) 举例如下: decimal d=12.30M;
-
字符型
char
-
字符型只能存放一个字符,它固定占用两个字节,能存放一个汉字。 字符型用 char 关键字表示,存放到 char 类型的字符需要使用单引号括起来,例如 ‘a’、‘中’ 等。 举例如下: char c = ‘A’;
-
注意:字符型只能使用单引号。 双引号代表字符串类型。
-
-
布尔类型
bool
- C# 语言中,布尔类型使用 bool 来声明,它只有两个值,即 true 和 false。
枚举
-
枚举是一个比较特别的存在,它是一个被命名的整形常量的集合,一般用它来表示
动作状态、类型
等 -
枚举是直接在
命名空间、类或结构
中使用 enum 关键字定义的。所有常量名都可以在大括号内声明,并用逗号分隔。 -
枚举声明的位置通常是:
命名空间的下面, 类的外面
, 表示这个命名空间下, 所有的 类都可以访问这个枚举 -
枚举使用
enum
关键字来声明,与类同级,枚举本身可以有修饰符,但枚举的成员始终是公开的,不能有访问修饰符,枚举本身的修饰符仅能使用Public
和internal
。 -
枚举类型的枚举成员均为静态,且默认值为Int32类型
-
每个枚举成员均具有相关联的常数值,此值的类型就是枚举的底层数据类型,每个枚举成员的常数值必须在该枚举的底层数据类型的范围之内,如果没有明确指定底层数据类型则默认的数据类型是int类型。
-
枚举类型不能相同,但枚举的值可以相同,也就是枚举前边的符号不能相同,但是后边的值就可以相同
-
枚举是隐式密封的,不允许作为基类派生子类
-
申明枚举和申明枚举变量是两个概念
申明枚举: 相当于是创健一个自定义的枚举类型
申明枚举变量: 使用申明的自定义枚举类型创健一个枚举变量//枚举名 以E或者E_开头 作为我们的命名规范 enum E_自定义枚举名 { 自定义枚举项名字, //0 枚举中包裹的整形常量第一个默认值是0 下面会 *依次累加* 自定义枚举项名字1,//1 可以给枚举赋值 下面会 *依次累加* 自定义枚举项名字2,//2 } //声明枚举 enum E_PlayerType { Main, other } //申明枚举变量 //自定义的枚举类型 变量名 = 默认值;(自定义的枚举类型,枚举项) E_PlayerType playerType = E_PlayerType.Main; if(playerType == E_PlayerType.Main) { Console.WriteLine("主玩家逻辑"); }else if(playerType == E_PlayerType.Other) { Console.WriteLine("其它玩家逻辑"); } //枚举和switch是天生一对 E_PlayerType playerType = E_PlayerType.Main; switch (playerType) { case E_PlayerType.Main: Console.WriteLine("主玩家逻辑"); break; case E_PlayerType.Other: Console.WriteLine("其它玩家逻辑"); break; default: break; }
结构体
-
结构体是一种自定义变量类型
-
类似枚举需要自己定义
struct
-
它是数据和函数的集合 在结构体中可以申明名种变量和方法
-
作用:用来表现在关系的数据集合 比如用结构体表现学生,动物,人类等等
//结构体一般写在namespace语句块中 //结构体关键字 struct 自定义结构体名 //注意结构体名字 我们的规范是 帕斯卡命名法 { //第一部分 //变量 //第二部分 //构造函数(可选) //第三部分 //函数 } //实例:结构体类型的创建 //学生类型 struct Student { //变量 //结构体申明的变量不能直接初始化 //变量类型可以写 任意类型包括结构体 但是 不能是自己的结构体 public string name; public char sex; public int age; //构造函数(可选) //自定义构造函数 一般是用于在外面方便初始化 public Student(string name,char sex,int age) { //作用:快速给结构体字段赋初值 //而且必须给每一个字段都赋初值 //新的关键字 this 代表自己 下面即代表结构体里的变量 this.name = name; this.sex = sex; this.age = age; } //函数 //注意在结构体中的函数方法目前不需要加static关键字 void Speak() { //函数中可以直接使用结构体内部申明的变量 Console.WriteLine("我的名字是(o),我今年(l}岁",name,age); } //可以根据需求写无数个函数 }
-
结构体的构造函数 (可以重载)
- 结构体默认的构造函数,开发者不能创建默认构造(即无参构造),必须有参数
- 没有返回值,函数名必须和结构体名相同
- 如果申明了构造函数那么必须在其中对所有变量数据初始化
结构体的使用
//定义一个学生变量
Student st1;
//学生结构内变量赋值
st1.name = "xiaoming";
st1.age = 16;
st1.sex = 'M';
st1.Speak();
//使用构造函数初始化
Student s2 = new Student("xiaohong","M",18);
1.2 引用类型
引用类型不包含存储在变量中的实际数据,但它们包含对变量的引用,存储实际数据的地址,存储在堆内存-它变我也变。
换句话说,它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置的引用类型有:object、dynamic 和 string。
- 从内存上看,值类型是在栈中的操作,而引用类型是在堆中的操作。
(导致 => 值类型存取速度快,引用类型存取速度慢。) - 从本质上看,值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针或引用。
(值类型是具体的那个数值所占用的空间大小,而引用类型是存放那个数值的空间地址。) - 从来源上看,值类型继承自System.ValueType,引用类型继承自System.Object。
- 特别的:结构体是值类型,类和string是引用类型。
字符串(String)类型 特殊
-
string
非常的特殊它具备值类型的特征-它变我不变,因为重新赋值时 会在堆中重新分配空间。 -
string
虽然方便但是有一个小缺点就是频繁的改变string
重新赋值会产生内存垃圾
. -
字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。
string str = "runoob.com";
-
字符串
本质是char数组
- 转为char数组
ToCharArray()
方法
string str="小明"; Console.WriteLine(str[0]); //转为char数组 char[]chars = str.ToCharArray();
- 转为char数组
-
字符串拼接
str = string.Format("{0}{1}",1,3333); //13333
-
正向查找字符位置
string str="小明"; int index=str.IndexOf("明"); //从前往后查找 index = 1 没有返回-1
-
反向查找字符位置
string str="小明"; int index=str.LastIndexOf("明"); //从后往前查找index = 1 没有返回-1
-
移除指定位置后的字符
string str = "小明大明大壮"; string s = str.Remove(2);//不会改原字符串 会返回新字符串"小明" 第二个位置及之后的都移除 //参数一 开始位置 参数二 字符个数 string s = str.Remove(2,2);//不会改原字符串 会返回新字符串"小明大壮"
-
替换指定字符串
string str = "小明大明大壮"; string s = str.Replace("大明","小小");//不会改原字符串 会返回新字符串"小明小小大壮"
-
大小写转换
string str = "rmb"; //小写转大写 string s = str.ToUpper();//不会改原字符串 会返回新字符串"RMB" //大写转小写 string t = s.ToLower();//不会改原字符串 会返回新字符串"rmb"
-
字符串截取
string str = "小明大明大壮"; //截取从指定位置开始之后的字符串 string s = str.Substring(2);//不会改原字符串 会返回新字符串"大明大壮" //参数一 开始位置 参数二 字符个数 //不会自动的帮助你判断是否越界你需要自己去判断 string s = str.Substring(2,2);//不会改原字符串 会返回新字符串"大明"
-
字符串切割
string str ="1,2,3,5,6,7,8"; string[] strs = str.Split(',');//通过逗号切割 放入数组
StringBuilder
-
修改字符串而不创建新的对象,需要频繁修改和拼接的字符串可以使用它,可以提升性能
-
使用前需要引用命名空间
//引用命名空间 才可使用StringBuilder using Systen.Text; //初始化 直接指明内容 StringBuilder str = new StringBuilder("123321"); //本质还是字符数组
-
容量
- StringBuilder存在一个容量的问题
- 初始化时会预留未使用空间,往里面增加时,超过空间容量会自动扩容(16,32,64)
- 获得容量
Console.WriteLine(str.Capacity);//查看容量
- 获得字符长度
Console.WriteLine(str.Length);
-
增删查改替换
//增加 str.Append("4444"); //1233214444 str.AppendFormat("{0}{1}",100,999); //1233214444100999 //插入 str.Insert(0,"小明"); //小明1233214444100999 //删除 str.Remove(0,7); //14444100999 //查 Console.WriteLine(str[1]); //4 //改 str[0] = 'A'; //A4444100999 //替换 str.Replace("1","9"); //改变原字符串 A9444100999 //清空 str.Clear(); //空 //判断StringBuilder是否和某一个字符串相等 if(str.Equals("123")) { }
数组
-
概念:同一变量类型的数据集合
-
一定要掌握的知识:申明,遍历,增删查改
-
所有的变量类型都可以申明为数组
-
它是用来批量存储游戏中的同一类型对象的 容器 比如所有的怪物,所有玩家。
-
一维数组
-
变量类型[ ] 数组名;/只是申明了一个数组但是并没有开辟空间
-
变量类型可以是我们学过的或者没学过的所有变量类型
-
变量类型[ ] 数组名=new 变量类型[ ]
//1.数组声明 int[] arr1; //变量类型[] 数组名 = new 变量类型[数组的长度]; int[] arr2 = new int[5];//这种方式相当于开了5个房间但是 房间里面的int值 默认为0 //变量类型[] 数组名 = new 变量类型[数组的长度]{内容1,内容2,内容3,........}; int[] arr3 = new int[5]1,2,3,4,5}; //变量类型[] 数组名 = new 变量类型{内容1,内容2,内容3,}; int[] arr4 = new int[]1,2,3,4,5};//后面的内容就决定了数组的长度 //变量类型【】数组名={内容,内容2,内容3,........}; int[] arr5 = {1,3,4,5,6}; //2.数组的长度 //数组变量名.Length arr3.Length; //3.获取数组中的元素 //数组中的下标和索引他们是从0开始的 //通过 索引下标 去获得数组中某一个元素的值时 //一定注意!!!!! 不能越界数组的房间号范围是 0~Length-1 arr3[0]; //求arr3数组下标为0的元素 //4.修改数组中的元素 arr3[0] = 99; //5.遍历数组通过循环快速获取数组中的每一个元素 for (int i = 0;i < arr3.Length; i++) { Console.WriteLine(arr3[i]); } //6.增加数组的元素 //数组初始化以后是不能够直接添加新的元素的 int[] arrnew = new int[6]; //搬家 for (int i=0;i < arr3.Length; i++) { arrnew[i] = arr3[i]; }
- 二维数组
-
二维数组是使用两个下标(索引)来确定元素的数组
-
两个下标可以理解成行标和列标,比如矩阵
//二维数组的声明 //变量类型[,] 二维数组变量名; int[,] arr;//申明过后会在后面进行初始化 //变量类型[,] 二维数组变量名 = new变量行[行,列]; int[,] arr2 = new int[3,3]; //变量类型[,] 二维数组变量名 = new 变量类型[行,列]{{0行内容1,0行内容2,0行内容3},{1行内容1,1行内容2,1行内容3}}; int[,] arr3 = new int[3,3] {{1,2,3}, {4,5,6}, {7,8,9}}; //变量类型[,] 二维数组变量名 = new 变量类型[,]{{0行内容1,0行内容2,0行内容3},{1行内容1,1行内容2,1行内容3}}; int[,] arr4 = new int[,] {{1,2,3}, {4,5,6}, {7,8,9}}; //变量类型[,] 二维数组变量名 = {{0行内容1,0行内容2,0行内容3},{1行内容1,1行内容2,1行内容3}}; int[,] arr5 = {{1,2,3}, {4,5,6}, {7,8,9}};
-
二维数组的使用
//1.二维数组的长度 //我们要获取行和列分别是多长 //总长度 arr5.Length; //得到多少行 arr5.GetLength(0); //得到多少列 arr5.GetLength(1); //2.二维数组的元素访问 arr5[2,2]; //3.修改二维数组中的元素 arr5[2,2] = 99; //4.二维数组的遍历 for (int i = 0; i < arr5.GetLength(0); i++) { for (int j = 0; j < arr5.GetLength(1); j++) { Console.Write(arr5[i,j] + "\t"); } //换行 Console.WriteLine(); } //foreach迭代遍历 //foreach循环,这种循环遍历数组和集合更加简洁。 //foreach性能消耗要大一点,所以能用for的尽量用for //使用foreach循环遍历数组时,无须获得数组和集合长度,无须根据索引来访问数组元素,foreach循环自动遍历数组和集合的每一个元素。 //注意:迭代遍历是只读的,不能修改 foreach (var item in arr5) { Console.WriteLine(item); //迭代遍历是只读的,不能写入 }
- 交错数组
-
交错数组是元素为数组的数组。交错数组元素的维度和大小可以不同。交错数组有时称为“数组的数组”
//1.数组的声明 //变量类型[][] 交错数组名; int[][] arr1; //变量类型[][] 交错数组名 = new 变量类型[行数][]; int[][] arr2 = new int[3][]; //变量类型[][] 交错数组名 = new变量类型[行数][]{一维数组1,一维数组2,......}; int[][] arr3 = new int[3][]{new int[]{1,2,3}, new int[]{1,2} new int[]{1}}}; //变量类型[][] 交错数组名 = new变量类型[][]{一维数组1,一维数组2,......}; int[][] arr4 = new int[][]{new int[]{1,2,3}, new int[]{1,2} new int[]{1}}}; //变量类型[][] 交错数组名 = {一维数组1,一维数组2,......}; int[][] arr5 = {new int[]{1,2,3}, new int[]{1,2} new int[]{1}}}; //2.数组的长度 //行 arr5.GetLength(0)); //得到某一行的列数 arr5[0].Length; //3.获取交错数组中的元素 //注意:不要越界 arr5[0][1]; //4.修改交错数组中的元素 arr5[0][1]=99; //5.遍历交错数组 for (int i = 0; i < arr5.GetLength(0); i++) { for (int j = 0; j < arr5[0].Length; j++) { Console.Write(arr5[i][j] + "\t"); } //换行 Console.WriteLine(); }
类和对象
基本概念
-
具有相同特征,具有相同行为,一类事物的抽象
-
类是对象的模板,可以通过类创建出对象,类的关键词
class
-
类一般申明在namespace语句块中
//类的声明 //命名:用帕斯卡命名法 //注意:同一个语句块中的不同类不能重名 class 类名 { //特征一成员变量 //行为一成员方法 //保护特征一成员属性 //构造函数和析构函数 //索器 //运算符重载 //静态成员 }
成员变量
基本规则
-
申明在类语句块中
-
用来描述对象的特征
-
可以是任意变量类型
-
数量不做限制
-
是否赋值根据需求来定
//枚举 enum E_SexType { } //结构体 struct Position { } //类 class Pet { } class Person { //特征一 成员变量 可以是任意变量类型 //姓名 string name="唐老狮"; //年龄 public int age; //性别 public E_SexType sex; //女朋友 *//如果要在类中申明一个和自己 相同类型 的成员变量时* //不能对它进行实例化 会进入死循环 内存溢出卡死 public Person gridFriend; //朋友 public Person[] Friends; //位置 public Position pos; //宠物 private Pet pet = new Pet(); }
访问修饰符
-
public 一 公共的 自已(内部)和别人(外部)都能访问和使用
-
private 一 私有的 自己(内部)才能防问和使用 不写默 认为private
-
protected 一 保护的 自己(内部)和子类才能访问和使用
-
目前决定类内部的成员 的 访问权限
- 成员变量的使用和初始值
- 值类型 来说数字类型 默认值都是
0
bool 类型false
- 引用类型的
null
- 看默认值的小技巧 default(int)
成员方法
基本概念
成员方法(函数用来表现对象行为)
-
申明在类语句块中
-
是用来描述对象的行为的
-
规则和函数申明规则相同
-
受到访问修饰符规则影响
-
返回值参数不做限制
-
方法数量不做限制
-
注意:
- 成员方法
不要加static
关键字 - 成员方法 必须实例化出对象 再通过对象来使用 相当于该对象执行了某个行为
- 成员方法 受到访问修饰符影响
class Person { public string name; public int age; //不带访问修饰符 默认为私有private bool IsAdult() { return age >= 18; } //可以使用成员变量 public void Speak(string str) { IsAdult(); //私有的内部才能使用的方法 Console.WriteLine("{0}说{1}",name,str) } } //使用 Person p = new Person p.name = "小明”; p.age = 18 p.Speak("你好"); if(p.IsAdult()) { p.Speak("我18岁了"); }
- 成员方法
成员属性
-
用于保护成员变量
-
为成员属性的获取和赋值添加逻辑处理
-
解决的局限性
- public一内外访问
- private一内部访问
- protected一内部和子类访问
- 属性
可以让
成员变量在外部
只能获取 不能修改 或者 只能修改不能获取
class Person { private string name; private int age; private int money; private bool sex; //朋友 public Person[] Friends; //属性的命名一般使用帕斯卡命名法 //注意: //1.默认不加会使用属性申明时的访问权限 //2.加的访问修饰符要低于属性的访问权限 //3.不能让get和set的访问权限都低于属性的权限 public string Name { get //可以+private { //可以在返回之前添加一些逻辑规则 取出来 解密 //意味着 这个属性可以获取的内容 return name; } set { //可以在设置之前添加一些逻辑规则 存进去 加密 //value关键字 用于表示 外部传入的值 name = value; } } //自动属性 //作用:外部能得不能改的特征 //如果类中有一个特征是只希望外部能得不能改的又没什么特殊处理 //那么可以直接使用自动属性 public int age { //没有再get和set中写逻辑的需求或者想法 get; set; } } //使用 Person p = new Person(); p.Name = "小明"; //执行set Person p2 = new Person(); p2.name ="大明"; p2.ag = 16; //添加到p的Person[] Friends里 p.AddFriend(p2);
索引器
基本概念
-
让对象可以像数组一样通过索引访问其中元素,使程序看起来更直观,更容易编写
-
比较适用于在
类中有数组变量
时使用可以方便的访问和进行逻辑处理//访问修饰符 返回值 this[参数类型 参数名,参数类型 参数名......] { //内部的写法和规则和成员属性相同 get{}; set{}; } //实例 class Person { private string name; private int age; private int[,] arr; private Person[] friends; //索引器重载 public int this[int i,int j] { get { return arr[i,j]; } set { arr[i,j] = value; } } //索引器重载 public string this[string str] { get { switch(str) { case "name": return this.name; case "age": return age.ToString(); } return ""; } } //索引器 public Person this[int index] { get { //可以写逻辑的根据需求来处理这里面的内容 //不为空,不越界则返回 否则返回null 不写这个若数组为空 越界 则会报错 if(friends == null||friends.Length - 1 < index) { return null; } return friends[index]; } set { //value代表传入的值 //可以写逻辑的根据需求来处理这里面的内容 if(friends == null) { friends = new Person[]{ value }; } else if(idnex > friends.Length - 1) { //自己定了一个规则 如果索引越界 就默认把最后一个朋友顶掉 friends[friends.Length - 1] = value; } friends[index] = value; } } } //索引器的使用 Person p = new Person(); P[0] = new Person(); //访问Person this[int index]的 set Console.WriteLine(p[0]); //访问Person this[int index]的 get 得到
静态成员
-
关键词
static
-
静态成员
- 成员:字段、属性、方法
- 静态:跟对象没有任何关系,
只跟类有关系
-
静态成员在何时开辟的内存
- 程序开始运行时就会分配内存空间,所以我们就能直接使用
-
静态成员在何时释放内存
- 在程序结束的时候才会释放
-
普通的实例成员,每有一个对象,就有一个该成员
- 而静态成员,跟对象没有关系,所以无论有多少个对象,静态成员都只有一个
- 例: 实例成员【name】,每有一个人,就会有对应的一个名字
- 而静态成员【Population】,跟对象没有关系,无论有多少个实例对象,人口数量只有一个
-
静态方法中是不可以访问非静态的成员的
- 不能访问非静态的字段、属性
- 不能调用非静态的方法
-
非静态方法中是可以访问静态成员的
- 能访问静态的字段、属性
- 能调用静态的方法
-
静态方法是可以有重载
//自定义静态成员 class Test { //静态变量 public static float PI = 3.1415926f; //普通成员变量 public int testInt = 100; //静态方法 public static float Calccircle(float r) { //静态函数中不能使用非静态成员 //r = PI*testInt; return PI * r * r; } //普通成员方法 public int TestFun(int x) { //非静态函数可以使用静态成员 生命周期原因 return x*PI; } } //静态成员的使用 //因为 静态成员 的生命周期原因 可以 不实例化 直接点出静态方法成员 Test.Calccircle(2.0f); Console.WriteLine(Test.PI); // 普通成员变量 只能将对象 实例化 出来后才能点出来使用 不能无中生有 Test t = new Test(); Console.WriteLine(t.TestFun(2));
静态类
特点
-
静态类中
只能存在静态成员
,不能存在非静态的成员
-
静态类是
不能进行实例化
的 -
作用:
- 将常用的静态成员写在静态类中方便使用
- 静态类不能被实例化,更能体现工具类的唯一性
- 比如Console就是一个静态类
static class Tools { //静态成员变量 public static int testIndex = 0; public static void TeseFun() { } public static int TestIndex { get; set; } }
-
静态构造函数
- 只有一种写法
- static 类名()
- 静态构造函数必须无参数
- 静态构造函数在什么时候才会调用
- 静态构造函数在程序运行期间
只会执行一次
- 在第一次访问该类的时候调用
- 用这个类去new一个对象
- 用这个类去访问某个静态成员
- 用这个类去调用某个静态方法
- 静态构造函数在程序运行期间
- 如果有继承关系
- 静态构造函数的执行顺序是:
- 先执行子类的静态构造,再执行父类的静态构造
- 先子后父
- 静态构造有什么作用
- 一般用于对静态成员进行初始化
static class Tools { //静态成员变量 public static int testIndex = 0; //静态构造函数 不管在不在静态类 只会自动调用一次 //作用:初始化静态成员 static Tools() { } //普通构造 new的时候就会调用 public Tools() { } } //用这个类去访问某个静态成员 静态方法 会调用静态构造函数 只会调用一次 初始化静态成员 Tools.testIndex;
- 只有一种写法
-
拓展方法
概念
- 为现有非静态变量类型添加新方法
- 作用
- 提升程序拓展性
- 不需要再对象中重新写方法
- 不需要继承来添加方法
- 为别人封装的类型写额外的方法
- 特点
- 一定是
写在静态类中
- 一定是个
静态函数
- 第一个参数为拓展目标
- 第一个参数用this修饰
- 一定是
static class Tools { //为int拓展了一个成员方法 //成员方法 是需要 实力化对象后 才 能使用的 //value 代表 使用该方法的 实例化对象 public static void NewValue(this int value,int r) { //拓展的方法 的逻辑 } //为Person类拓展了一个成员方法 Person类为非静态类 静态类不能为静态类拓展方法 //拓展方法和原有的方法名重复了 用的会是原方法 public static void NewPerson(this Person value) { //拓展的方法 的逻辑 } } //拓展方法的使用 int i = 10; //第一个参数为本身的值 //第一个参数后的参数才会作为这个函数的参数 i.NewValue(20); //在此处NewValue(this int value)的value 代表 i的值 即10
类对象
-
基本概念
- 类的申明 和类对象(变量)申明是两个概念
- 类的申明 类似 枚举 和 结构体的申明 类的申明相当于申明了一个自定义变量类型
- 而对象 是类创建出来的
- 相当于申明一个指定类的变量
- 类创建对象的过程 一般称为实例化对像
- 类对象 都是引用类型的
//对象基本语法 //类名 变量名; //类名 变量名 = null; (null代表空) //类名 变量名 = new 类名(); //实例: class Person { //特征一成员变量 //行为一成员方法 //保护特征一成员属性 //构造函数和析构函数 //索器 //运算符重载 //静态成员 } //实例化对象 Person p; Person P2 = nu11; //null代表空不分配堆内存空间 Person p3 = new Person(); //相当于一个人对象 Person p4 = new Person(); //相当于又是一个人对象 //注意 //虽然他们是来自一个类的实例化对像 //但是他们的 特征 行为等等信息 都是他们 独有 的 //千万干万 不要觉得他们是共享了数据 两个人 你是你 我是我 彼此没有关系
密封类
基本概念
-
密封类是使用
sealed
密封关键字修饰的类 -
作用:
-
在面向对象程序的设计中,密封类的主要作用就是不允许最底层子类被继承
-
可以保证程序的规范性、安全性
-
-
意义:加强面向对象程序设计的规范性、结构性、安全性
//让Father类无法被继承 sealed class Father { }
1.3 指针型
指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。
声明指针类型的语法:
type* identifier;
char* cptr;
int* iptr;
2. 函数(方法)
-
本质是一块具有名称的代码块
-
可以使用函数(方法)的名称来执行该代码块
-
函数(方法)是封装代码进行重复使用的一种机制
-
函数(方法)的主要作用
- 封装代码
- 提升代码复用率(少写点代码)
- 抽象行为
-
函数写在哪里
- cass语句块中
- structi语句块中
//static 返回类型 函数名(参数类型参数名1,参数类型参数名2,···)
{
//函数的代码逻辑;
//函数的代码逻辑;
//......
return 返回值; //如果有返回类型才返回
}
//1.关于static 不是必须的
//2-1.关于返回类型 引出一个新的关键字 void(表示设有返回值)
//2-2.返回类型可以写任意的变量类型 14种变量类型 + 复杂数据类型(数组、枚举、结构体、类class)
//3.关于函数名 使用帕斯卡命名法命名 myName(驼蜂命名法)MyName(帕斯卡命名法)
//4-1.参数不是必须的,可以有0~n个参数 参数的类型也是可以是任意类型的 14种变量类型+复杂数据类型(数组、枚举、结构体、类c1ass) 多个参数的时候需要用逗号隔开
//4-2.参数名 驼峰命名法
//5.当返回值类型不为void时 必须通过新的关键词 return 返回对应类型的内容(注意:即使是void也可以选择性使用return)
-
有参有多返回值函数
static int[]Calc(int a,int b) { int sum a +b; int avg sum 2; int[] arr = {sum,avg }; return arr; } //或者 static int[]Calc(int a,int b) { int sum = a + b; int avg = sum / 2; return new int[] { sum,avg }; }
-
关于return
- 即使函数没有返回值,我们也可以使用return
- return可以直接不执行之后的代码,直接返回到函数外部
引用参数ref
-
添加了ref关键词的参数
- 传递的就不是值了
- 而是
地址
- 而如果没有赋初值,是没有地址的
- 所以ref参数
一定是个变量
- 所以ref参数的
实参一定是赋过初值
-
所以ref一般加在
值类型
参数的前面 -
使用应用参数,
无论是形参还是实参
前面都要加ref
关键词//未加ref 相当于让函数内的value = 外部传过来的x(值类型),原本外部x值是不会变的 static void ChangeArrayValue(ref int value) { value = 99; } static void Main(string[] args) { //ref传入的变量必须在外部赋值out不用 int x = 2; ChangeArrayValue(ref x); Console.WriteLine(x); //结果输出99 //它们可以解决 在函数内部改变外部传入的内容 里面变了 外面也要变 }
输出参数out
-
添加了out关键词的参数
- 参数就成了一个输出的通道
离开方法之前形参必须赋值
- 实参
必须是一个变量
- 传递的实参一般是
值类型
-
使用输出参数,
无论是形参还是实参
前面都要加out
关键词//未加ref 相当于让函数内的value = 外部传过来的x(值类型),原本外部x值是不会变的 static void ChangeArrayValue(out int value) { //out传入的变量必须在内部赋值ref不用 value = 99; } static void Main(string[] args) { int x = 2; ChangeArrayValue(out x); Console.WriteLine(x); //结果输出99 //它们可以解决 在函数内部改变外部传入的内容 里面变了 外面也要变 }
变长参数 params 和参数默认值
-
变长参数关键字
params
-
params int[] 意味着可以传入n个int参数 n可以等于0 传入的参数会存在arr数组中
-
注意:
- params关键字后面必为
数组
- 数组的类型可以是任意的类型
- 函数参数可以有 别的参数和params关键字修饰的参数
- 函数参数中只能最多出现一个params关键字 并且一定是在
最后一组参数
前面可以有n个其它参数
- params关键字后面必为
//实例
static int Sum(params int[] arr)
{
int sum = 0;
for (int i=0;i<arr.Length;i++)
{
sum += arr[i];
}
return sum;
}
//使用
Sum();
Sum(1,2,3,4,5,6,7,8,1);
参数默认值
-
有参数默认值的参数一般称为可选参数
-
作用是当调用函数时可以不传入参数,不传就会使用默认值作为参数的值
-
注意:
- 支持多参数默认值每个参数都可以有默认值
- 如果要混用 可选参数 必须写在 普通参数后面
static void Speak(string test, string str="我设什么话可说") { Console.WriteLine(str); }
函数重载
重载概念
-
在同一语句块
class
或者struct
中 -
函数(方法)名相同
参数的数量不同
-
参数的数量相同,但
参数的类型
或顺序不同
-
作用:
- 命名一组功能相似的函数,减少函数名的数量,避免命名空间的污染
- 提升程序可读性
-
注意:
- 重载 和返回值类型 无关,只和
参数类型
,个数
,顺序
有关 - 调用时程序会自己根据传入的参数类型判断使用哪一个重载
//实例 static int CalcSum(int a,int b) { return a + b; } //参数数量不同 static int CalcSum(int a,int b,int c) { return a + b + c; } //数量相同,类型不同 static float Calcsum(int a,float b) { return a + b; } //使用 调用时程序会自己根据传入的参数类型判断使用哪一个重载 CalcSum(1,2); CalcSum(1.2,3); Ca1csum(1,2.3f);
- 重载 和返回值类型 无关,只和
递归函数
-
方法自己调用自己
-
多个方法之间来回调用
-
使用递归时
一定要有出口
-
使用递归时,一定概要慎重慎重再慎重…
//实例 求x的阶层 x! int Fun(int x) { if(x == 0) return 1; else return x*Fun(x-1); }
构造函数
基本概念
-
在实例化对象时会调用的用于初始化的函数
-
构造函数没有返回值
- 也不能写返回值类型
-
如果不写 默认 存在一个无参构造函数
-
构造函数的写法
- 没有返回值
- 函数名和类名必须相同
- 没有特殊需求时一般都是public的
- 构造函数可以被重载
this
代表当前 调用该函数的对象 自己
-
注意:
- 如果不自己实现无参构造函数而实现了有参构造函数
- 会失去默认的无参构造
//实例 class Person { public string name; public int age; //无参构造 //类中是允许自己申明无参构造函数的 //结构体是不允许 public Person() { name = “小明”; age = 18; } //有参构造 public Person(int age) { //this代表当前调用该函数的对象自己 this.age = age; } //构造函数可以被重载 public Person(string name,int age) { //this代表当前调用该函数的对象自己 this.name = name; this.age = age; } } //构造函数的调用 //实例化对象,调用无参构造 Person p1 = new Person(); //实例化对象,调用有参构造 Person p2 = new Person("唐老狮",18);
-
构造函数特殊写法
- 可以通过
this
重用构造函数代码
//函数后面this(),表示先调用无参构造函数,再调用本函数 public Person(string name,int age):this() { this.name = name; this.age = age; } Person p new = Person("小明",18); //会先到this(),即先调用无参构造函数 //函数后面this(int age),表示先调用有参构造函数Person(int age),再调用本函数 //会先把传进来的age参数,传给有参构造函数Person(int age); public Person(string name,int age):this(age) { this.name = name; this.age = age; } Person p new = Person("小明",18); //会先到this(age),即先调用有参构造函数
- 可以通过
析构函数
-
当引用类型的堆内存被回收时,会调用该函数
-
对于需要手动管理内存的语言(比如C++),需要在析构函数中做一些内存回收处理
-
但是C#中存在自动垃圾回收机制GC
-
所以我们几乎不会怎么使用析构函数。除非你想任素一个对象被垃圾回收时,做一些特殊处理
-
注意:在Unity开发中析构函数几乎不会使用,所以该知识点只做了解即可
//当引用类型的堆内存被回收时 //析构函数 是当垃圾 真正被回收的时候才会调用的函数 ~Person() { }
垃圾回收机制
-
垃圾回收,英文简写Gc (Garbage Collector)
-
垃圾回收的过程是在遍历堆(Heap)上动态分配的所有对象
-
通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用
-
所谓的垃圾就是没有被任何变量,对象引用的内容
-
垃圾就需要被回收释放
-
垃圾回收有很多种算法,比如
- 引用计数(Reference Counting)
- 标记清除(Mark Sweep)
- 标记整理(Mark Compact)
- 复制集合(Copy Collection)
-
注意:
-
Gc只负责堆(Heap)内存的垃圾回收
-
引用类型
都是存在堆(Heap)中的,所以它的分配和释放都通过垃圾回收机制来管理 -
栈(Stack)上的内存是由系统自动管理的
-
值类型
在栈(Stck)中分配内存的,他们有自己的申明周期,不用
对他们进行管理,会自动分配和释放
-
-
C#中内存回收机制的大概原理
-
0代内存 1代内存 2代内存
-
代的概念:
-
代是垃圾回收机制使用的一种算法(分代算法)
-
新分配的对象都会被配置在第0代内存中
-
每次分配都可能会进行垃圾回收以释放内存(0代内存满时)
-
-
在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步
-
标记对像从根(静态字段、方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象
不可达对象就认为是垃圾 -
搬迁对象压缩堆 (挂起执行托管代码线程) 释放末标记的对象 搬迁可达对象 修改引用地址
-
-
大对象总被认为是第二代内存 目的是减少性能损耗,提高性能
-
不会对大对象进行搬迁压缩 85088字节(83kb) 以上的对象为大对象
手动触发垃圾回收(Loding场景加载)
- 手动触发垃圾回收的方法
- 一般情况下我们不会频繁调用
- 都是在Loadingi过场景时才调用
GC.Collect();
3. 继承
基本概念
-
一个类A继承一个类B
-
类A将会继承类B的所有成员
-
A类将拥有B类的所有特征和行为
-
被继承的类 称为 父类、基类、超类
-
继承的类称为 子类、派生类
-
子类可以有自己的特征和行为
-
特点
- 单根性 子类只能有一个父类
- 传递性子类可以间接继承父类的父类
class Teacher { //姓名 public string name; //职工号 public int number; //介绍名字 public void SpeakName() { Console.WriteLine(name); } } //继承Teacher类 class TeachingTeacher:Teacher { //具备Teacher类 特征和行为,并添加自己的特征和行为 //科目 public string subject; public new string name; //表示覆盖父类的name 默认也覆盖 写new为了规范化 public void SpeakSubject() { Console.WriteLine(subject+"老师"); } } //使用 TeachingTeacher th = new TeachingTeacher(); th.name = "小明"; th.number = 1; th.subject = "unity"; th.SpeakName(); th.SpeakSubject();
里式替换准则
基本概念
-
里氏替换原则是面向对象七大原则中最重要的原则
-
概念:
- 任何父类出现的地方,子类都可以替代
-
重点:
- 语法表现一父类容器装子类对象,因为子类对象包含了父类的所有内容
-
作用:
- 方便进行对象储和管理
//基本实现 class Gameobject { } class Player:Gameobject { public void PlayerAtk() { Console.WriteLine("玩家攻击"); } } class Monster:Gameobject { public void MonsterAtk() { Console.WriteLine("怪物攻击"); } } //使用 //里式替换准则 用父类容器 装载子类对象 Gameobject player = new Player(); Gameobject monster = new Monster(); Gameobject[] objects = new Gameobject[] {new Player(),new Monster()}; //用父类容器 装载子类对象 通过父类使用子类独特方法 //is 和 as
is 和 as
基本概念
-
is:判断对象是否是执行类对象
- 返回值:bool 是为真 不是为假
-
as:将一个对象转换为指定类对象
-
返回值:指定类型对像
-
成功返回执行类型对象,失败返回null
-
-
基本语法
- 类对象 is 类名 该语句块 会有一个bool返回值 true 和 false
- 类对象 as 类名 该语句块 会有一个对象返回值 对象 和 null
//is:判断一个对象是否是指定类对象 //返回值:boo1是为真不是为假 if(player is Player) { }else if(player is Monster) { } //as:将一个对象转换为指定类对象 //返回值:指定类型对像 //成功返回指定类型对象,失败返回null Player p = player as Player; //若失败 p = null; if(player is Player) { Player p = player as Player; p.PlayerAtk(); //一步到位 (player as Player).PlayerAtk(); }
继承中的构造函数
继承中构造函数 基本概念
-
特点
- 当申明一个子类对象时
- 先执行父类的构造函数
- 再执行子类的构造函数
-
注意:
- 父类的无参构造很重要 当子类实例化 会
默认调用父类无参构造
- 子类可以通过
base
关键字 代表父类 调用父类构造
- 父类的无参构造很重要 当子类实例化 会
-
继承中构造函数的执行顺序
- 父类的父类的构造 一 >。。。父类构造 一> 。。。一>子类构造
//实例 class Father { //当子类实例化 会默认调用父类无参构造 public Father() { Console.WriteLine("Father无参构造"); } public Father(int i) { Console.WriteLine("Father有参构造"); } } class Son:Father { //base和this相似 base关键字代表父类 this关键字代表自己 //通过base调用指定父类有参构造 解决若父类没有无参构造 而子类实例化报错问题 public Son(int i):base(i) { Console.WriteLine("Son有参1"); } public Son(int i,int y):this(i) { Console.WriteLine("Son有参2"); } } //使用 Son s = new = Son(1,2); //先到Son(int i,int y):this(i) 有this(i) 则到Son(int i):base(i) //有base 则到父类有参构造Father(int i) //所以输出为: Father有参构造 Son有参1 Son有参2
Object 所有类基类
万物之父
-
关键字:
object
-
概念:
object
是所有类型的基类 它是一个类(引用类型)
-
作用:
- 可以利用里氏替换原则,用object容器装所有对像
- 可以用来表示不确定类型,作为函数参数类型
//使用 //引用类型 object o = new Son(); if(o is son) { (o as Son).Speak(); } //值类型 object o2 = 1f; //用强转使用 float f = (float)o2; //特殊的string类型 object str = "123123"; string str2 = str.Tostring(); string str2 = str as string;
Object中的方法
object 中的静态方法:
-
静态方法
Equals
判断两个对象是否相等- 最终的判断权,交给左侧对像的Equals方法
- 不管值类型引用类型都会按照左侧对象Equls方法的规则来进行比较
class test { } //值类型 object.Equals(1,1); //true //引用类型 比的是地址是否相同 Test t1 = new Test(); Test t2 = new Test(); object.Equals(t1,t2); //false
-
静态方法
ReferenceEquals
- 比较两个对象是否是相同的引用,主要是用来比较引用类型的对象
- 值类型对象返回值始终是false
//值类型 object.ReferenceEquals(1,1); //false object.ReferenceEquals(t1,t2); //true
object中的成员方法:
-
普通方法
GetType
- 该方法在反射相关知识点中是非常重要的方法,之后我们会具体的讲解这里返回的Type类型。
- 该方法的主要作用就是获取对象运行时的类型Type
- 通过Type结合反射相关知识点可以做很多关于对象的操作。
Test t = new Test(); Type type = t.GetType();
-
普通方法
Memberwiseclone
- 该方法用于获取对象的浅拷贝对象,口语化的意思就是会返回一个新的对象,但是新对象中的引用变量会和老对象中一致。
class Test { public int i = 1; public Test Clone() { //返回一个克隆类 因为Memberwiseclone是保护类型的 无法在外部调用 所以要内部返回 return Memberwiseclone() as Test; } } //使用 Test t = new Test(); public t = t.Clone();
object中的虚方法:
-
虚方法
Equals
- 默认实现还是比较两者是否为同一个引用,即相当于ReferenceEquals.
- 但是微软在所有值类型的基类System.ValueType中重写了该方法(上面的Equals),用来比较值相等。
- 我们也可以重写该方法,定义自己的比较相等的规则
-
虚方法
GetHashCode
- 该方法是获取对象的哈希码
- (一种通过算法算出的,表示对象的唯一编码,不同对象哈希码有可能一样,具体值根据
- 我们可以通过重写该函数来自己定义对像的哈希码算法,正常情况下,我们使用的极少
-
虚方法ToString
- 该方法用于返回当前对象代表的字符串,我们可以重写它定义我们自己的对象转字符串规则
- 该方法非常常用。当我们调用打印方法时,默认使用的就是对像的ToString,方法后打印出来的内容。
装箱拆箱
-
发生条件
- 用object存值类型(装箱)
- 再把object转为值类型(拆箱)
-
装箱
- 把值类型用引用类型存储
- 栈内存会迁移到堆内存中
-
拆箱
- 把引用类型存储的值类型取出来
- 堆内存会迁移到栈内存中
-
好处:不确定类型时可以方便参数的存储和传递
-
坏处:存在内存迁移,增加性能消耗
//装箱 object v = 3; //拆箱 int intValue = (int); public void Fun(params boject[] arr) { } Fun(1,2.0f,34.5,"123",new Son());
4. 多态
多态按字面的意思就是“多种状态”
-
让继承同一父类的子类们在执行相同方法时有不同的表现(状态)
-
主要目的:
- 同一父类的对像执行相同行为(方法)有不同的表现
-
解决的问题
- 让同一个对像有唯一行为的特征
//解决的问题 同一个对象执行不同方法的问题 Father f new Son(); f.SpeakName(); (f as Son).SpeakName();
-
如果父类想要子类可以重写该函数
- 那么父类的该函数必须是一个虚函数
- [访问修饰符] virtual 返回值类型 函数名(参数列表)
-
子类该怎么重写
- [访问修饰符] override 返回值类型 函数名(参数列表)
class GameObject { //虚函数virtual 用来给子类重写 public virtual void Atk() { Console.WriteLine("游戏对象进行攻击); } } class Player:GamgeObject { //override重写虚函数 public override void Atk() { //base的作用 //代表父类可以通过bas来保留父类的行为 base.Atk(); Console.WriteLine("玩家对象进行攻击); } } //使用 Object p = new Player(); //都是调用子类的Atk方法 p.Atk(); (p as Player).Atk();
抽象类
概念
-
被抽象关键字abstract修饰的类
-
特点:
- 不能被实例化的类
- 可以包含抽象方法
- 继承抽象类必须重写其抽象方法
//实例 //抽象不能被实例化 abstract class Thing { //抽象类中封装的所有知识点都可以在其中书写 public string name; //可以在抽象类中写抽象函数 }
抽象方法(函数)
-
用
abstract
关键字修饰的方法 -
特点:
- 只能在抽象类中申明
- 没有方法体
- 不能是私有的
- 继承后必须实现 用
override
重写
-
什么时候使用:
- 父类中的行为不太需要被实现的,只希望子类去定义具体的规则的可以选择抽象类然后使用其中的抽象方法来定义规则(定义怪物类,各种怪物继承后必须实现不同抽象方法,相当于实现不同战斗方式)
//实例 abstract class Fruits { public string name; //抽象方法 是不能有函数体的 //不能是私有的 //继承后必须实现 用override重写 public abstract void Bad(); } class Apple:Fruits { //实现父类抽象方法 不然报错 //虚方法和抽象方法 都可以被子类无限重写 public override void Bad() { } }
5. 单例、接口和范型
单例
单例模式是比较常见的一种设计模式,目的是保证一个类只能有一个实例,而且自行实例化并向整个系统提供这个实例,避免频繁创建对象,节约内存。
例如,界面上只能有一个鼠标指针,并且该鼠标指针能被所有程序访问。同样的还有,企业解决方案可以与管理到特定系统连接的单网关对象进行对接。
- 如果一个对象在声明时直接实例化【new】。
- 在访问这个类的时候调用
- 实例化的时间点最早,在静态构造之前就执行了
饿汉式
- 这是比较常见的写法,在类加载的时候就完成了实例化,避免了多线程的同步问题。当然缺点也是有的,因为类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,内存就浪费了。
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
普通的懒汉式 (线程不安全,不可用)
- 这是懒汉式中最简单的一种写法,只有在方法第一次被访问时才会实例化,达到了懒加载的效果。但是这种写法有个致命的问题,就是多线程的安全问题。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果,所以这种写法不可采用。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
同步方法的懒汉式 (可用)
- 这种写法是对getInstance()加了锁的处理,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,为了改进这种写法,就有了下面的双重检查懒汉式。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类 (可用,推荐)
-
这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。
-
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
枚举 (可用、推荐) java
- 线程安全问题。因为Java虚拟机在加载枚举类的时候会使用ClassLoader的方法,这个方法使用了同步代码块来保证线程安全。
- 避免反序列化破坏对象,因为枚举的反序列化并不通过反射实现
public class Singleton {
private Singleton(){
}
public static enum SingletonEnum {
SINGLETON;
private Singleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
}
}
在代码中,我们首先将Singleton类的构造函数设置为private私有的,然后在Singleton类中定义一个静态的枚举类型SingletonEnum。
在SingletonEnum中定义了枚举类型的实例对象Singleton,再按照单例模式的要求在其中定义一个Singleton类型的对象instance,其初始值为null;我们需要将SingletonEnum的构造函数改为私有的,在私有构造函数中创建一个Singleton的实例对象;最后在getInstance()方法中返回该对象。
接口
基本概念
-
接口是行为的抽象规范
-
它也是一种自定义类型
-
关键字
interface
-
接口申明的规范
- 不包含成员变量
- 只包含方法、属性、索引器、事件
- 成员不能被实现
- 成员可以不用写访问修饰符,不能是私有的
- 接口不能继承类,但是可以继承另一个接口
-
接口的使用规范
- 类可以继承多个接口
- 类继承接口后,必须实现接口中所有成员
-
使用时机
:把行为抽象出去,让需要的类去继承(同一种类型,不同行为或不同类型,同种行为用接口) -
特点:
- 它和类的申明类似
- 接口是用来继承的
- 接口不能被实例化,但是可以作为容器存储对象
//1.接口的声明 接口关键字:interface //一句话记忆:接口是抽象行为的基类” //接口命名规范帕斯卡前面加个I interface IFly { //1.不包含成员变量 //2.只包含方法、属性、索引器、事件 //成员不能是私有的 不写默认公共的 类继承接口后,必须实现接口中所有成员 void Fly(); //自动属性 string Name { get; set; } //索引器 int this[int index] { get; set; } //事件 event Action doSomthing; } //接口的使用 class Animal { } //类可以继承1个类,n个接口 //继承了接口后 必须实现其中的内容 并且必须是public的 class Person:Animal,Fly { //接口也遵循里氏替换原则 //可以与virtual 虚方法配合使用 让子类重写 public virtual void Fly() { } public string name { get; set; } public int this[int index] { get; { return 0; } set { } } public event Action doSomthing; } //2.接口继承接口时不需要实现 //待类继承接口后类自己去实现所有内容 //注意:显示实现接口时不能写访问修饰符 interface ISuperAtk { void Atk(); } interface IAtk { void Atk(); } class Player IAtk,ISuperAtk { //显示实现接口就是用接口名.行为名去实现 void IAtk.Atk() { } void ISuperAtk.Atk() { } public void Atk() { } } //显示实现接口 使用 IAtk ia = new Player(); ISuperAtk isa = new Player(); Player() p = new Player(); ia.Atk(); isa.Atk(); p.Atk();
泛型
- 泛型实现了类型参数化,达到代码重用目的
- 通过类型参数化来实现同一份代码上操作多种类型
- 申明泛型时 泛型相当于类型占位符
- 定义类或方法时使用替代符代表变量类型
当真正使用
类或者方法时再具体指定类型
泛型类
-
class 类名<泛型占位字母>
- 类中的字段类型、方法参数、方法返回值,都可以使用类中的泛型
class Test<T> { public T value; } class Testclass<T,T1,t2> //T相当于占位符 实现了类型参数化 { public T value1; public T1 value2; public T3 value3; } //使用 //<int,string>告诉了T类型为int,T1类型String TestClass<int,string,Test<int>> t = new TestClass<int,string,Test<int>>(); t.value1 = 10; //Testclass<T,T1>中的变量value1为int类型 赋值为10 t2.value2 = "123123"; //Testclass<T,T1>中的变量value2为string类型 赋值为"123123"
泛型接口
-
interface 接口名<泛型占位字母>
interface TestInterface<T> { T Value { get; set; } } //继承 继承要填类型 class Test:TestInterface<int> { //接口实现 public int Value { get; set; } }
-
泛型方法
//普通类中的泛型方法 class Test { public void TestFun<T>(T value) { } //重载 public void TestFun<T>() { //用泛型类型 在里面做一些逻辑处理 T t = default(T); //default()得到类型默认值 因为T类型未知 } //重载 T作为返回值 public T TestFun<T>(string v) { return default(T); } //多个返回值 public T TestFun<T,K,M>(T t,K k,M m) { } } //使用 Test t = new Test1(); t.TestFun<string>("123123");
//泛型类中的泛型方法 class Test<T> //与上面的class Test 不是同一个类了 虽然名字一样 但是加了泛型 { public T value; //这个不叫泛型方法因为T是泛型类申明的时候就指定在使用这个函数的时候 //我们不能再去动态的变化了 public void TestFun<T t> { Console.WriteLine(t); } //这个叫泛型方法 public void TestFun<K k> { Console.WriteLine(k); } } //使用 函数参数可以变化的才叫 泛型方法 Test<int> t = new Test<int>(); t.TestFun<string>("123"); t.TestFun<float>(1.2f);
泛型的作用
-
不同类型对象的相同逻辑处理就可以选择泛型
-
使用泛型可以一定程度避免装箱拆箱
-
举例:优化ArrayList
//不同类型对象 相同逻辑处理 不同类型的增删查改 定义一次就行 class ArrayList<T> { private T[] arr; public void Add(T value) { } public void Remove(T value) { } }
泛型约束
-
关键字
where
-
让泛型的类型有一定的限制
-
泛型约束一共有6种
- 值类型 where 泛型字母:struct
- 引用类型 where 泛型字母:class
- 存在无参公共构造函数 where 泛型字母:new()
- 某个类本身或者其派生类 where 泛型字母:类名
- 某个接口的派生类型 where 泛型字母:接口名
- 另一个泛型类型本身或者派生类型 where 泛型字母:另一个泛型字母
//1.值类型约束 class Test<T> where T:struct //给占位符约束 值类型 { public T value; public void TestFun<K>(K k) where K:struct //给占位符约束 值类型 { } } //使用 Test<int> t = new Test<int>(); //占位符必须为 值类型 t.TestFun<float>(2.0f);
//2.引用类型约束 class Test<T> where T:class //给占位符约束 引用类型 { public T value; public void TestFun<K>(K k) where K:class //给占位符约束 引用类型 { } } //使用 Test<object> t = new Test<object>(); //占位符必须为 引用类型 t.TestFun<object>(new object);
//3.引用类型约束 class Test<T> where T:class //给占位符约束 引用类型 { public T value; public void TestFun<K>(K k) where K:class //给占位符约束 引用类型 { } } //使用 Test<object> t = new Test<object>(); //占位符必须为 引用类型 t.TestFun<object>(new object);
//4.无参公共构造约束 class Test<T> where T:new() { public T value; public void TestFun<K>(K k) where K:new() //给占位符约束 无参公共构造 { } } class Test1 { public void Test() //必须是公共的不然使用 无参公共构造约束 会报错 { } } //使用 Test<Test1> t = new Test<Test1>(); //有公共的无参构造类或结构体 抽象类不行 t.<Test1>(new Test1); Test<int> t1 = new Test<int>(); //值类型也行 Test<object> t2 = new Test<object>(); //引用类型也行
//5.类约束 class Test<T> where T:Test1 //给占位符约束 类约束 Test1为类名 { public T value; public void TestFun<K>(K k) where K:Test1 //给占位符约束 类约束 Test1为类名 { } } //使用 Test<Test1> t = new Test<Test1>(); //占位符必须为 约束的类名Test1 或其派生类(子类) t.TestFun<Test1>(new Test1);
//6.接口约束 //定义接口 interface IFly { } //继承接口 class Test1:IFly { } class Test<T> where T:IFly //给占位符约束 接口约束 IFly为接口名 某个接口派生类型 { public T value; public void TestFun<K>(K k) where K:IFly //给占位符约束 接口约束 IFly为接口名 { } } //使用 Test<IFly> t = new Test<IFly>(); t = new Test1(); //或者 其接口派生类型(接口子类) Test<Test1> t = new Test<Test1>(); t = new Test1();
//7.另一个泛型约束 interface IFly { } //继承接口 class Test1:IFly { } class Test<T,U> where T:U //给占位符约束 约束为:T和U一样或者T为U的派生类 { public T value; public void TestFun<K,V>(K k) where K:V //给占位符约束 U为另一个泛型约束 { } } //使用 Test<Test1,IFly> t = new Test<Test1,IFly>(); //Test1为IFly派生类
//约束的组合使用 class Test<T> where T:class,new() //约束为引用类型且有公共无参类型 { } //多个泛型有约束 T 和 K分别约束 class Test8<T,K> where T:class,new() where K:struct { }
密封方法
基本概念 用密封关键字sealed
修饰的重写函数
-
作用:让虚方法或者抽象方法之后
不能再被重写
-
特点:和
override
一起出现//实例 class Person { public override void Eat() { } }
6. 命名空间
概念
-
命名空间是用来组织和重用代码的
-
关键字
namespace
-
作用
- 就像是一个工具包,类就像是一件一件的工具,都是申明在命名空间中的
-
使用
- 不同命名空间中相互使用需要引用命名空间或指明出处
不同
命名空间中允许有同名类
- 命名空间可以包裹命名空间
//实例 //引用命名空间 using myGame using myGame.UI namespace myGame { //类 class Gameobject { } //命名空间可以包裹命名空间 namespace UI { class Image { } } } namespace na2 { //类 class Program { static void Main(string[] args); //不同命名空间中相互使用 需要引用命名空间或指明出处 Gameobject g = new Gameobject(); Image i = new Image(); //上面没有引用 或者引用的有同名应该指明出处 myGame.Gameobject g = new myGame.Gameobject(); myGame.UI.Image i = new myGame.UI.Image(); } }
-
修饰类的访问修饰符
- public – 公共的
- internal – 只能在该程序集中使用 命名空间中的类默认为
internal
- abstract – 抽象类
- sealed – 密封类
- partial – 分部类
7. 区别
结构体和类的区别
区别概述
-
结构体和类最大的区别是在存储空间上的,因为结构体是值,类是引用
-
因此他们的存储位置一个在栈上,一个在堆上
-
通过之前知调点的学习,我相信你能够从此处看出他们在使用的区别一值和引用对象在赋值时的区别
-
结构体和类在使用上很类似,结构体甚至可以用面向对象的思想来形容一类对象。
-
结构体具备着面向对象思想中封装的特性,但是它不具备继承和多态的特性,因此大大减少了它的使用频率
-
由于结构体不具备继承的特性,所以它不能够使用protected保护访问修饰符
细节区别
- 结构体是值类型,类是引用类型
- 结构体存在栈中,类存在堆中
- 结构体成员不能使用orotectedi访问修饰符,而类可以
- 结构体成员变量申明不能指定初始值,而类可以
- 结构体不能申明无参的构造函数,而类可以
- 结构体申明有参构造函数后,无参构造不会被顶掉
- 结构体不能申明析构函数,而类可以
- 结构体不能被继承,而类可以
- 结构体需要在构造函数中初始化所有成员变量,而类随意
- 结构体不能被静态
static
修饰(不存在静态结构体),而类可以 - 结构体不能在自己内部申明和自已一样的结构体变量,而类可以
结构体的特别之处
- 结构体可以继承接口因为接口是行为的抽象
如何选择结构体和类
- 想要用
继承和多态时
,直接淘汰结构体,比如玩家、怪物等等
- 对象是数据集合时,优先考虑结构体,比如
位置、坐标等等
- 从值类型和引用类型赋值时的区别上去考虑,比如
经常被赋值传递的对象
,并且
改变赋值对象,原对象不想跟着变化时,就用结构体。比如坐标、向量、旋转等等
抽象类和接口的区别
相同点
- 都可以被继承
- 都不能直接实例化
- 都可以包含方法申明
- 子类必须实现未实现的方法
- 都遵循里氏替换原则
区别
- 抽象类中可以有构造函数:接口中不能
- 抽象类只能被单一继承:接口可以被继承多个
- 抽象类中可以有成员变量;接口中不能
- 象类中可以申明成员方法,虚方法,抽象方法,静态方法;接口中只能申明没有实现的抽象方法
- 抽象类方法可以使用访问修饰符:接口中建议不写,默认public
如何选择抽象类和接口
- 表示对像的用抽象类,表示行为拓展的用接口
- 不同对象拥有的共同行为,我们往往可以使用接口来实现
- 举个例子:
- 动物是一类对像,我们自然会选择抽类;而飞翔是一个行为,我们自然会选择接口。
三.C#进阶
1. 简单数据结构类
ArrayList
ArrayList的本质
-
ArrayList:是一个C#为我们封装好的类
-
它的本质是一个object类型的数组
-
ArrayListi类帮助我们实现了很多方法
-
比如数组的增删查改
//声明