C#基础
文章目录
一、C#数据类型
1.元组
1.元组的介绍
- 元组在C# 7.0及以后版本才支持
- 用于处理各种数据类型的组合
2.元组类型的定义及使用
//第一种定义方式
(double,int) value1 = (4.5,3); //定义了一个元组变量
(double,int,float) value2 = (4.5,3,0.5f); //定义了一个元组变量
Console.WriteLine(Value2.Item1); //输出元组类型变量的值
Console.WriteLine(Value2.Item2);
Console.WriteLine(Value2.Item3);
//第二种定义方式
(double vd,int vi,float vf) value3 = (4.5,3,0.5f); //定义了一个元组变量 直接写明变量名称
Console.WriteLine(Value2.vd); //输出元组类型变量的值
Console.WriteLine(Value2.vi);
Console.WriteLine(Value2.vf);
//第三种定义方式,通过隐式类型var
var value4 = (vd:4.5,vi:3,vf:0.5f);
Console.WriteLine(Value2.vd); //输出元组类型变量的值
Console.WriteLine(Value2.vi);
Console.WriteLine(Value2.vf);
//第4种定义方式,也是通过隐式类型var
var (v1,v2)=(1,2.4f);
Console.WriteLine(v1); //输出元组类型变量的值
Console.WriteLine(v2);
2.Object和动态类型
1.基本的介绍
- Objetc:在C#中,所有类型都是直接或间接从System.Object继承的,所以可以将任何变量赋值给Object类型的变量
- dynamic:动态类型,在编译时不做检查。在运行时确定类型
- Var:隐式类型,通过赋值语句来确认类型,但是确认后不可再修改,编译时确定
1.定义和使用
Object obj = 123456;
obj="c#";
Console.WriteLine(obj); //可以将任意类型的变量赋值给Object
dynamic dyc = "c#";
dyc = 123456;
Console.WriteLine(dyc); //可以赋值不同类型的变量
var i = 12;
//i="c#"; 这个语句编译不通过,i通过赋值语句确认了是int类型,就不可以再将string类型的值赋值给i
3.类型默认值
- 任何引用类型:null
- 内置整数和浮点数类型:0
- bool:false
- char:‘\0’
- enum:0
- strcut:所有值类型字段为默认值,所有引用类型字段为null
4.作用域冲突问题
1.内层作用域的变量会覆盖外层作用域的同名变量
2.示例
//这样能正常编译
internal class Program
{
static int Temp = 1000;
static void Method()
{
Console.WriteLine(Temp);
}
}
//这样会报错
internal class Program
{
static int Temp = 1000;
static void Method()
{
Console.WriteLine(Temp);
int Temp = 100; //会提示在Temp变量声明之前无法使用,由于在本地作用域声明了和外层作用域同名的变量,隐藏了外层的Pragram.Temp变量。
}
}
//这样会成功编译,但输出的值是100
internal class Program
{
static int Temp = 1000;
static void Method()
{
int Temp = 100;
Console.WriteLine(Temp);
}
}
二、C#运算符
1.关系运算符
在C#中,== 和 equals 都是用来比较两个对象是否相等的方法,但它们之间有一些关键的区别。
- 运算符 ==:
== 是C#中的比较运算符,用于引用类型的比较。
当使用 == 运算符来比较两个引用类型对象时,它会检查两个引用是否指向内存中的同一个对象,即它们是否完全相同。
换句话说,它比较的是对象的引用(是不是指向一个地址),而不是它们的内容。 - 方法 equals:
equals 是一个方法,通常用于比较两个对象的内容是否相等。
object str1 = "abc";
object str2 = "ab";
str2 = str2 + "c";
bool result1 = str1==str2; //值是false,因为不是指向同一个引用
bool result2 = object.Equals(str1,str2);//值是true,因为内容相等
object str1 = "abc";
object str2 = "abc";
bool result1 = str1==str2; //值是true,因为指向同一个引用 c#中 相同的字符串共用一个堆区内存,这里两个"abc"其实同一个堆区内存
object str1 = "abc";
object str2 = "ab";
str2 = "ab" + "c";
bool result1 = str1==str2; //值也是true,因为指向同一个引用
bool result3 = object.ReferenceEquals(str1,str2); //比较对象的引用
2.算术运算符
- 算术运算符计算的时候,要考虑溢出等问题
- 取余运算符%常用场景是限定值的范围,比如输入值取余100,取值范围就在0-100。例如平时的随机数
3.字符串的操作
- 子字符串的截取
string str = "Hello-world";
string subStr = str.Substring(0,2); //从第0个字符开始截取2个字符
- 字符串的分隔
string ip = "192.168.1.209";
string[] ipArr = ip.Split('.'); //按','分隔字符,返回一个字符串数组
- 字符串的格式化
string strIp = string.Format("{0}+{1}+{2}+{3}", ipArr[0], ipArr[1], ipArr[2], ipArr[3]); //用{0}之类的进行占位
string strIp2 = $"{ipArr[0]}+{ipArr[1]}+{ipArr[2]}+{ipArr[2]}"; //在字符串前加$符号,后续字符串内的可以直接用{}传入对应要显示的字符串变量
- 字符串取消转义
string str3 = @"D:\Tools\"; //加@符号,后续字符串不会做特殊处理,只显示原本内容
5 字符串的Join方法:串联字符串数组的所有元素,其中在每个元素之间使用指定的分隔符
List<string> list = new List<string>();
list.Add("12");
list.Add("34");
string str = string.Join(",", list); //将list中所有字符串,按“,”号分隔串联
三、C#方法
1.out和ref传值
- 两个都是地址传递
- out传值前,变量可以没初始化。传入的时候,传入的参数必须带out修饰符
- ref传值前,变量必须有值。传入的时候,传入的参数必须带ref修饰符
{
int a1 = 10;
int a2 = 20;
int result;
Program program = new Program();
program.Add_Out(a1, a2,out result); //可以不用初始化,但必须带out修饰符传入 由于Add不是静态函数,所以需要声明Add所在类Program的变量,通过变量去调用
//int a3;
//program.Add_Ref(a1, a2, ref a3);//传入的ref变量必须有值,带ref修饰符传入。这里会报错
int a4 = 0;
program.Add_Ref(a1, a2, ref a4); //这样是正常的;
Console.WriteLine(a4);
Console.ReadLine();
}
void Add_Out(int a,int b,out int result)
{
result = a + b;
}
void Add_Ref(int a, int b, ref int result)
{
result = a + b;
}
2.访问修饰符
- public
- private
- protected
- internal:同一程序集中的任何代码都可以访问该类型或成员
- protected internal:该类型或成员可由对其进行声明的程序集或另一个程序集中的派生class中的任何代码访问
- private protected:该类型或成员可以通过从class派生的类型访问,这些类型在其包含程序集中进行声明
一个方法没写访问修饰符,默认为private
一个类没有指定访问修饰符,默认为internal
四、C#类
1.属性
namespace ConsoleApp2
{
internal class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
/*myClass.MyProperty_4 = 10;*/ //会报错,只能类内的方法访问
}
}
public class MyClass
{
private int myVar; //变量,为私有的
public int MyProperty //属性,是一种语法糖,目前MyProperty是可读可写
{
get;
set;
}
public int MyProperty_2 //属性,是一种语法糖,目前MyProperty是只读
{
get;
}
//public int MyProperty_3 //属性,是一种语法糖,不允许没有get访问器,因为从操作系统的概念上来讲,设置一个变量却不读取他,将没有意义
//{
// set;
//}
public int MyProperty_4 //属性,表示可以在类外读取,却只能在类内设定
{
get;
private set;
}
void Add()
{
MyProperty_4 = 4;
}
}
}
2.构造函数
- 继承:this() 对应的构造函数
namespace ConsoleApp2
{
internal class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass(); //由于继承原因,虽然调用的public MyClass(),但会先调用一次public MyClass(int a)
MyClass myClass2 = new MyClass(1,2); //同上
Console.WriteLine(myClass.MyProperty);
Console.ReadKey();
}
}
public class MyClass
{
private int myVar;
public int MyProperty
{
get { return myVar; }
set { myVar = value; }
}
/*
这里空参的构造函数继承了传入一个int型参数的构造函数
在上面实例化类的时候,因为没有传参,所以调用的是这个无参构造。但由于有继承,会先调用public MyClass(int a)这个构造函数,然后继续调用public MyClass(),最终结果为100
*/
public MyClass():this(0)
{
myVar = 100;
}
public MyClass(int a)
{
myVar = a;
}
public MyClass(int a)
{
myVar = a;
}
/*
这里继承了public MyClass(int a),调用的时候也会优先调用public MyClass(int a),再调用自身
*/
public MyClass(int a, int b) : this(a)
{
}
}
}
3.类与结构体
1.类与结构体的差异
- 结构体是值类型 在栈区分配内存,类是引用类型,在堆区分配内存
- 结构体 也可以有属性和方法。结构体有隐藏的无参构造函数,而且无法重写。任何时候都会存在
- 结构体的new方法不是生成一个新的对象,而是在用构造函数给变量赋值。
- 类的new方法是生成一个新的对象,在堆区内存
- 结构体是值类型在栈区存储,速度较快,数据量较小的数据类型适合结构体
- 类里面定义的值类型的变量在new一个新对象的时候其实是存放在堆内存的
2.结构体的陷阱
- 当创建一个类,再在类中定义一个结构体。直接调用类的对象去修改结构体的属性,会提示“无法修改“…”的返回值,因为它不是变量”。
public struct MyStruct
{
private int myVar;
public int MyProperty
{
get { return myVar; }
set { myVar = value; }
}
}
public class MyClass
{
private int myVar;
public int MyProperty
{
get { return myVar; }
set { myVar = value; }
}
private MyStruct myStruct1;
public MyStruct MyStruct1
{
get { return myStruct1; }
set { myStruct1 = value; }
}
}
internal class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
myClass.MyStruct1.MyProperty(10); //会报错提示“无法修改“...”的返回值,因为它不是变量”。
//因为myClass是引用类型,MyStruct是值类型,这里实际会调用
public MyStruct MyStruct1
{
get { return MyProperty; }
}
//但由于结构体是值传递的,返回的是栈上的一个临时的备份。无法赋值到本身去,所以没有意义,编译器从源头就禁用了这个操作
//如果直接创建Mystruct这个结构体变量
MyStruct myStruct = new MyStruct();
myStruct.MyProperty = 20; //这样是有效的,相当于直接给栈区的内存赋值,类似于 int a;a =10;
//前面在类中赋值结构体的属性,类似于创建了一个函数,将a进行值传递返回,由于是值传递,不会修改本身的值
}
}
2.类(引用类型)传入函数算引用传入
- 函数内部对形参的值进行修改,可以改变外部实参的值,因为引用类型在传参时,是将变量的地址给了形参,当形参被修改时,意味着其地址所指向的堆空间中的值被修改了,此时外部实参也指向的该地址所指向的堆空间,因此间接的对外部实参的值进行改变。
- 而string类型比较特殊,string类型是引用类型,但是在函数内部是无法修改外部实参的值的,因为string类型的变量在被赋予新值的时候,不会将原值擦去而填上新的值,而是会在堆中重新开辟空间,在空间中放入新的值,从而变量的地址也会改变,变成新空间的地址。
internal class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
SetA(myClass); //在函数内修改,外面的对象值会修改,相当于引用传递
string str = "12";
SetString(ref str); //在函数内修改,外面的对象的值不会修改,因为string类型不定长,修改内容会新开辟堆区内存,是一个新的变量了
}
public static void SetA(MyClass myClass)
{
myClass.MyProperty = 1000;
}
public static void SetString(ref string str)
{
str = "234";
}
}
public class MyClass
{
private int myVar;
public int MyProperty
{
get { return myVar; }
set { myVar = value; }
}
}
五、封装,继承,多态
1.抽象类
- 抽象类是在类前面加上abstract修饰符,抽象类的定义和正常的类一致。但抽象类不可以实例化,常用作父类
//抽象类的定义
public abstract class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
- 一个子类只能有一个父类,但是可以继承多个接口
2.多态
- 在父类中的方法前增加virtual,表示该方法为虚方法。可以在子类中重写
- 在子类的方法前加new修饰符可以隐藏父类的方法。
- 如果是重写虚方法,用父类引用子类的实例,也可以调用子类重写的方法,但是如果是隐藏的话,父类引用子类的实例无法调用子类的方法
- 在方法前加abstract是抽象方法,抽象方法不能有方法主体,而且必须在抽象类中
- 抽象方法也可以用override修饰在子类重写,但是必须在子类重写抽象方法,不然无法编译
namespace ConsoleApp2
{
internal class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
dog.Speak(); //输出"汪汪汪_重写虚方法"
dog.Speak2(); //输出"汪汪汪_隐藏父类方法"
dog.Speak3(); //输出"汪汪汪_重写抽象方法"
Animal An = new Dog();
An.Speak(); //输出"汪汪汪_重写虚方法"
An.Speak2(); //输出叫声 如果是重写虚方法,用父类引用子类的实例,也可以调用子类重写的方法,但是如果是隐藏的话,父类引用子类的实例无法调用子类的方法
An.Speak3(); //输出"汪汪汪_重写抽象方法"
Console.ReadLine();
}
}
public abstract class Animal
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
public virtual void Speak() //虚方法,用virtual修饰,可以在子类重写
{
Console.WriteLine("叫声");
}
public void Speak2() //普通方法
{
Console.WriteLine("叫声");
}
public abstract void Speak3();
}
public class Dog : Animal
{
public override void Speak() //重写虚方法
{
Console.WriteLine("汪汪汪_重写虚方法");
}
public new void Speak2() //隐藏了父类同名的方法。
{
Console.WriteLine("汪汪汪_隐藏父类方法");
}
public override void Speak3()
{
Console.WriteLine("汪汪汪_重写抽象方法");
}
}
}
五、异常处理
1.异常捕获
- 通常用
try,catch,finally,throw
这几个关键字来实现 - Exception是所有异常类的基类
catch(System.IndexOutOfRangeException ex) //如果是下标溢出相关异常才会进入到这个异常捕获当中
{
}
catch (Exception ex) //如果没有catch能够对应的上错误,会由Exception捕获
{
}
2.抛出异常
- 通过
throw
关键字抛出异常
static void Main(string[] args)
{
while (true)
{
try
{
int a;
string value = Console.ReadLine();
bool result = Int32.TryParse(value, out a);
if(result)
{
Console.WriteLine(a);
}
else
{
throw new Exception("不可输入非数字"); //如果输入的值无法转换成int32类型,则throw Exception类型的错误,内容为"不可输入非数字"
}
}
catch(System.IndexOutOfRangeException ex)
{
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
- 也可以自己创建异常类
internal class ValueErrorException : Exception //自己创建异常类,继承自Exception
{
public ValueErrorException(string msg) :base(msg) //自定义有参的构造函数,通过传入的参数设定Message
{
}
}
internal class Program
{
static void Main(string[] args)
{
while (true)
{
try
{
int a;
string value = Console.ReadLine();
bool result = Int32.TryParse(value, out a);
if(result)
{
Console.WriteLine(a);
}
else
{
throw new ValueErrorException("不可输入非数字"); //抛出自定义的异常类
}
}
catch(ValueErrorException ex) //专门捕获自定义的异常类
{
Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
}
}
}
}