Tostring(); GetType();GetHashCode();Equals();这四个方法是object的;
File.readAllBytes(string path) //byte[]以字节形式读取;
File.writeAllBytes //以字节的形式写入;
byte[] buffer = File.ReadAllBytes(@"D:\Users\Ronnie\Desktop\1.txt");
把一个对象给ToString打印出来的是它的命名空间;
常用的编码有 UTF-8 GB2312(简体) GBK(简体加繁体) ASCII unicode
Default拿到的是默认的ANSI; utf7 utf8 utf32三个编码格式共同组成了unicode;
string s= Encoding.GetEncoding("GB2312").GetString(buffer);
string s= Encoding.GetEncoding("GBK").GetString(buffer);
string s= Encoding.UTF8.GetString(buffer);
string s= Encoding.Default.GetString(buffer);
string str = "今天天气好晴朗处处好风光";
byte[] buffer = Encoding.Default.GetBytes(str);
File.writeAllBytes(@"D:\Users\Ronnie\Desktop\1.txt",buffer); //没有这个文件的话,会给你创建一个,有的话 会给你覆盖掉
string[] contents = File.ReadAllLines(@"D:\Users\Ronnie\Desktop\1.txt",Encoding.Default); //以行的形式读取文件中的内容;
foreach(string item in contents)
{
}
string str = File.ReadAllText(@"D:\Users\Ronnie\Desktop\1.txt",Encoding.Default);
ReadAllLines,ReadAllText只能读文本文件.txt;
ReadAllBytes不仅能读文本文件,还能读其他类型的文件多媒体等;因为我们所有的文件都是以字节的形式存储的;
ReadAllLines可以精确到操作文本文件的每一行数据;如果一个文本文件要对里面的每一行进行操作,我们需要使用readAllLines();
当然如果只想把文本文件内容读出来展示那么 ReadAllText()最简单了;
绝对路径和相对路径
绝对路径:通过给定的这个路径直接能在我的电脑中找到这个文件.
相对路径:文件相对于应用程序的路径;
这个应用程序指的是bin.Debug下的.exe可执行文件,相对路径就是指文件相对于这个应用程序的路径;
如果把文件放到了bin.debug下,那么string str = File.ReadAllText("1.txt",Encoding.Default);
在编程当中尽量使用相对路径,在不同电脑上都适合;
File.WriteAllLines(@"D:\Users\Ronnie\Desktop\1.txt",new string[]{"sdad","sdasd"}); //以行的形式写入的,写一行换一行;
File.WriteAllText(@"D:\Users\Ronnie\Desktop\1.txt","sadsadadasda");
writeAllBytes WriteAllLines WriteAllText这三种方式都会覆盖原数据
File.AppendAllText(@"D:\Users\Ronnie\Desktop\1.txt","sadsadadasda"); //不会覆盖原来的内容,而是在原内容上追加;
File类能操作任意类型的文件,因为有readAllBytes() writeAllBytes();代码写起来简单;但是File类最大的缺点是它只能读小文件,因为它读文件的方式是一下子读出来,读的过程中很浪费内存,如果读大文件的话要用到文件流;
往ArrayList里面添加数据不用考虑长度,不用考虑数据类型,但是拿数据的时候很麻烦,因为拿出来的都是object类型,还得把它转成你需要的类型;因此我们产生了泛型集合;
创建了泛型类型,那么添加数据的类型就确定了,这点跟数组一样,但是泛型集合的长度跟ArrayList一样是没有限定的,
//创建泛型对象集合;
List<int> list = new List<int>;
list.Add(1); //添加单个元素
list.AddRange(new int[]{1,2,3,4,5}); //添加集合
list.AddRange(list); //也可以添加自己,因为自己也是int类型;
for(int i = 0;i < list.count;i++)
{
}
list.clear();
list.Reserve();
list.sort();
list.Insert();
list.Remove();
list.InsertRange();
int[] nums = list.ToArray(); //将list泛型集合转换为int[],能够转化为什么类型的数组取决于它是什么泛型集合;
List<int> listTwo = nums.toList(); //将int[]转换为list泛型集合;
也就只有list<>泛型集合能赚对应的数组,ArrayList不行,因为ArrayList元素的类型不是统一的,
ArrayList和HashTable不太用了,因为取值的时候不方便,
装箱:就是将值类型转换为引用类型;
拆箱:就是将引用类型转换为值类型;
int n = 10;
object o = n;//装箱
int nn = (int)o; //拆箱;
ArrayList list = new ArrayList();
for(int i = 0; i < 1000000; i++)
{
list.Add(i); //这个循环里面发生了100万次装箱操作;
}
List<int> list = new List<int>();
for(int i = 0; i < 1000000; i++)
{
list.Add(i); //没有发生100万次装箱操作;
}
//装箱会影响系统的性能,它在不停的进行类型转换,虽然比开辟空间快得多,但是它依然会浪费时间,因此在代码中要尽量避免装箱和拆箱
string str = "123";
int n = Convert.ToInt32(str);//看上去发生了拆箱操作,但是实际上没有 看两张类型是否发生了装箱或者拆箱,要看这两种类型是否存在继承关系;
//如果存在继承关系,则有可能发生了装箱和拆箱,如果不存在继承关系,则一定没有发生装箱和拆箱
int和string在内存上没有交集,一个在栈上,一个在堆上,他们之间没有继承关系,但是继承不一样了,内存则完全有可能有交集了;所以要看有没有拆装箱,就要看它们之间有没有继承关系;
接口也属于引用类型;
有继承关系,将值类型转换为了引用类型则发生了装箱;有继承关系,将引用类型转换为了值类型则发生了拆箱;
一旦出现装箱或者拆箱的代码,就会影响系统的时间,因此要避免出现这样的代码;
List<>对应ArrayList Dictionary对应HashTable
创建一个字典对象; Dictionary<int,string> dic = new Dictionary<int,string>();
dic.Add() 增加键值对;增加的时候键和值要与声明时候的类型一致;
跟HashTable一样,如果出现了2个一样的键就会出错;
dic.Add(1,"张三");
dic.Add(2,"李四");
dic.Add(1,"王五");//出现了一样的键,因此会导致出错
dic[1] = "新来的"; //这样添加的话不会抛异常;
遍历字典类型也需要用foreach()循环;
foreach(var item in dic.Keys)
{
Console.WriteLine("{0}{1}",item,dic[item]);
}
跟HashTable相比除了类型固定之外其他都是一样的;
字典类型还有一种遍历方式;
foreach(KeyVaulePair<int,string> kv in dic) 现在kv表示一对对的键值对,既能点出键,又能点出值; //以后就写这种;
{
Console.WriteLine("{0}{1}",kv.key,kv.value);
}
int i = 0;
string input = "abcdefg";
char[] chs = new char[input.length];
foreach(var item in input)
{
chs[i] = item;
i++;
}
用这种办法的话foreach就像for循环一样了;
统计welCome to china中每个字符出现的次数,不考虑大小写;
string str1 = "welcome to China";
string str = str1.ToUpper();
Dictionary<char,int> dic = new Dictionary<char,int>();
for(int i = 0 ; i < str.length;i++)
{
if(str[i] =='')
{
continue;
}
//如果集合中已经包含了键,
if(dic.ContainsKey(str[i]))
{
dic[str[i]]++;
}
else //这个字符在集合中是第一次出现;
{
dic[str[i]] = 1;
}
}
foreach(KeyVaulePair<char,int> kv in dic)
{
Console.WriteLine({0} {1},kv.Key,kv.Value);
}
FileStream和File的区别;
因为File在操作文件的时候是一次性读取;会对内存产生很大的负荷,所以File只能操作小的文件,因此我们用文件流读取;
FileStream读文件的时候是一点一点去读;对内存没什么压力,因此FileStream能够操作大文件,当然小文件也可以
FileStream 和 StreamReader和StreamWriter;
FileStream是操作字节的,意味着FileStream能够操作任意类型的文件,StreamReader和StreamWriter;是操作字符的;只能操作文本文件,
FileStream 和 StreamReader和StreamWriter;相对于File的好处是它们能够操作大文件;对内存压力小;
如果仅仅操作小文件我们用File类就OK了;
我们所谓的学习新内容或者学习新技术无非就是学习一个新的类,调用一下这个类的方法,传入一些新的参数,学习.net就是学习它这些类怎么去使用;
什么是大牛啊,就是他会的类比你会的类多,
针对文件做操作的类,基本上都是在IO,
FileStream不是静态类,因此可以创建对象;
FileStream fsRead = new FileStream(@"C:\Users\hhs\Desktop\1.txt",FileMode.OpenOrCreate,FileAccess.Read);
第一个参数要操作文件的路径,第二个参数针对文件做什么操作,第三个针对文件里的数据做什么操作,
byte[] buffer = new byte[1024*1024*5];
int r = fsRead.Read(buffer,0,buffer.Length); //r表示实际读取到的字节数,比如只有3.8M内容,但是读的话读了5M,这个r就是表示这有效的3.8M;r返回本次实际读取到的有效字节数
第一个参数表示每次读5M放到字节数组里,第二个参数表示从哪个地方开始放,我们写0,表示最开始那个点;第三个参数表示最多读多大内容出来;
string s = Encoding.Default.GetString(buffer,0,r); //将字节数组中每一个元素按照指定的编码格式解码成字符串;
第二个参数表示从哪里开始解码从0开始,第三个参数表示解码多少个,我们解码r个;
//关闭流
fsRead.close();
//释放流所占用的资源
fsRead.Dispose();
将创建文件流对象的过程写在using当中,会自动的帮助我们释放流所占用的资源;
using(FileStream fsWrite = new FileStream(@"C:\Users\hhs\Desktop\1.txt",FileMode.OpenOrCreate,FileAccess.Write)) //将创建对象的过程写在()里面
{
string str = "看我有没有把你覆盖掉";
byte[] buffer = Encoding.Default.GetBytes(str);
fsWrite.Write(buffer,0,buffer.Length); //第一个参数表示字节数组,第二个参数表示从哪里写入,第三个参数表示最多写多少;
}
在写聊天程序的时候,两边的编码格式要一样,不然会造成乱码;
实现一个文件的复制,其实就是先把文件读出来,然后写到你指定的地方,
string source = @"C:\Users\hhs\Desktop\1.txt";
string target = @"C:\Users\hhs\Desktop\2.txt";
public static void CopyFile(string source,string target)
{
//1.我们创建一个负责读取的流
using(FileStream fsRead = new FileStream(source,FileMode.Open,FileAccess.Read);)
{
//2.创建一个负责写入的流
using(FileStream fsWrite = new FileStream(target,FileMode.OpenOrCreate,FileAccess.Write);)
{
byte[] buffer = new byte[1024*1024*5];
//因为文件比较大,所以我们在读取的时候,应该通过一个循环去读;
while(true)
{
//返回本次实际读取到的字节数;
int r = fsRead.Read(buffer,0,buffer.Length);
//如果返回一个0,也就意味着什么都没读取到,读取完了,
if(r == 0)
{
break;
}
fsWrite.Write(buffer,0,r);
}
}
}
}
StreamWriter和StreamReader跟FileStream的区别是FileStream是操作字节的,因此它可以处理任意文件,而且可以处理大文件,
StreamWriter和StreamReader是操作字符的,用StreamReader来读取一个文本文件
using(StreamReader sr = new StreamReader(@"C:\Users\hhs\Desktop\1.txt",Encoding.Default))
{
while(!sr.EndOfStream) //判断是否读到了流的结尾,读到了文件的结尾,
sr.ReadLine();
}
//用它来操作文本文件比FileStream简单;
用StreamWriter来写入一个文本文件
using(StreamWriter sr = new StreamWriter(@"C:\Users\hhs\Desktop\1.txt",true)) //这个true表示是否在原内容上追加;
{
sw.Write("今天天气好晴朗")
}
//写入的时候比FileStream简单,但是它只能操作文本文件,不能操作多媒体文件,
因此我们得出结论
File类是操作小文件的,而且小文件的文本文件和多媒体文件都可以操作;因此小文件用它操作
FileStream是大小文件都可以操作的,而且不仅可以操作文本文件,还且还可以操作多媒体文件(全部适用);因此大文件的除了文本文件由他来操作
StreamReader和StreamWriter的话能操作大文件的文本文件;因此大文件的文本文件由它来操作;
面向对象中最重要的一部分 多态;多态把面向对象表现得淋漓尽致;
//多态,让一个对象能够表现出多种状态;
public class Person
{
private string _name;
public string Name
{
get{return _name;}
set{_name = value;}
}
public Person(string name)
{
this.Name = name;
}
public void SayHello()
{
Console.WriteLine("你好,我是人类");
}
}
public class Chinese:Person
{
public Chinese(string name):base(name)
{
}
public void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
}
public class Japanese:Person
{
public Japanese(string name):base(name)
{
}
public void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
}
public class Korea:Person
{
public Korea(string name):base(name)
{
}
public void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
}
public class American:Person
{
public American(string name):base(name)
{
}
public void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
}
Person[] pers = new Person[8];
Chinese cn1 = new Chinese("韩梅梅");
Chinese cn2 = new Chinese("李磊");
Japanese jp1 = new Japanese("井上");
Japanese jp2 = new Japanese("树下");
Korea kr1 = new Korea("金秀贤");
Korea kr2 = new Korea("全智贤");
American a1 = new American("科比");
American a2 = new American("奥尼尔");
Person[] pers ={cn1,cn2,jp1,jp2,kr1,kr2,a1,a2};
for (int i = 0;i<pers.Length;i++)
{
if(pers[i] is Chinese)
{
((Chinese)pers[i]).SayHello();
}
else if(pers[i] is Japanese)
{
((Japanese)pers[i]).SayHello();
}
else if(pers[i] is Korea)
{
((Korea)pers[i]).SayHello();
}
else
{
((American)pers[i]).SayHello();
}
}
//这一坨方法就是干了一件事Pers[i].SayHello();但是我们必须把它转成对应的类型,要不然Pers[i].SayHello();这样去写全是父类的,因为表现出来就是父类类型;
//因此我想办法让Person[i]既表现出来Chinese类型,又表现出来Japanese,Korea,American的类型;我们的多态就是来干这件事的,多态就是让一个对象能表现出多种类型
//实现多态的三种方法,1.虚方法 2.抽象类 3.接口;这三种方法能够帮助我们实现多态,让一个对象表现出来多种类型,
我们调的Pers[i].SayHello();是父类的SayHello,如果我们通过调用父类的SayHello来调到对应的子类的SayHello();就表现出来了多种状态的效果;
1.虚方法;
因此首先将父类的函数标记为虚方法,使用关键字virtual;
public virtual void SayHello()
{
Console.WriteLine("你好,我是人类");
}
标记为虚方法的效果就是这个函数可以被子类重新写一遍;
public override void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
public override void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
public override void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
public override void SayHello()
{
Console.WriteLine("你好,我是{0}",this.Name);
}
Pers[i].SayHello();//现在这一行代码就能实现上面的一坨代码,如果不加前面的virtual和override那么还是需要那一坨代码的,但是如果父类方法加了virtual
子类方法加了override则就只要一行就可以了,Pers[i]一个对象已经能够表现出多种状态了,这就是我们用虚方法实现多态;
虚方法的原理,Pers[i].SayHello()其实调的依然是父类的SayHello();但是父类的方法在子类中已经被重写了,不再调用自己的了,而是调用子类重写的那一个,具体调用哪个
子类重写的,取决于pers[i]里面装的是谁的对象,如果pers[i]装的是自己的对象,那么调用的就是自己的方法;装的谁的对象,调的是谁的方法;前提是利用虚方法实现多态;
如果不用虚方法,那么Pers[i].SayHello()调用的全部是父类自己的方法;这就是我们的多态,让一个对象表现出多种类型出来,写出通用的代码,最大效益得取消各个子类之间的差异性,
多态带给我们的好处是减少了很多很多的代码;增强了程序的可扩展性,pers[i]本质上依然是pers类型,只不过用虚方法模拟了一下它是多种类型,看起来它表现了Chinese,Japanese,Korea,American类型,其实本质上类型就是per[i];
虚方法实现多态的时候,子类的方法不一定要写override,如果要实现多态,那么加override,如果不实现多态,则不需要写override;
在使用虚方法实现多态的时候步骤,
1.需要在给定的类中提取出来一个父类,这个父类必须是题目给定出来的类,
2.必须将父类的函数标记为virtual;父类的函数对我们来说是有意义的,
经理属于员工,老板和老板娘才不属于员工;
虚方法的实现要在继承的基础上;
抽象类;
当父类中的方法,不知道如何去实现的时候,我们可以考虑将父类写成抽象类,将方法写成抽象方法;
//狗狗会叫,猫咪也会叫 用多态来实现;
public abstract class Animal
{
public abstract void Bark(); //抽象方法是不允许有方法体的;有大括号就叫做有方法体,大括号是空的叫做空实现,
//抽象方法没有内容,存在的意义是让子类重写;实现多态;
}
public class Dog : Animal
{
public override void bark()
{
Console.WriteLine("狗狗叫");
}
}
被abstract标记的方法是抽象方法,抽象方法没有方法体,如果知道public abstract class Animal里面能写什么内容的话,那么用虚方法实现多态,
public class Cat : Animal
{
public override void bark()
{
Console.WriteLine("猫咪叫");
}
}
方法的签名是指方法的参数和返回值;
抽象类是不允许创建对象的,接口也是不允许创建对象的,因为抽象类里面没有方法体,即使创建了也调用不到;
Animal a = new dog();
虽然确定a里面装的是子类对象,但是表现出来的依然是父类类型,因此调用的时候也只能调用自己的成员,但是由于我们用抽象类来实现了多态,
父类的两个方法被子类重写了,所以会调用子类的打招呼的方法,具体调用哪个函数取决于装的是谁的对象;
抽象类父类里面没实现,虚方法父类里面有实现,
父类的方法有实现用虚方法,父类的方法没有具体的实现用抽象类,
抽象类特点
1.抽象成员必须标记为abstract,并且不能有任何实现,
2.如果一个子类继承了一个抽象的父类,那么这个子类必须要重写这个抽象父类当中所有的抽象成员;(除非这个子类也是抽象类)
3.抽象类中可以写非抽象成员,并且子类没有必要去实现它们;因为无法创建抽象类对象,可能有人说没用,但是通过继承,子类可以用这些非抽象成员,因此是有意义的;
4.抽象成员必须在抽象类当中;我们不能在一个非抽象类当中写一个抽象成员;
5.抽象类不能被实例化;
6.抽象成员的访问修饰符不能是private;
7.抽象类是有构造函数的,虽然不能被实例化;
8.如果父类的抽象方法中有参数,那么,继承这个抽象父类的子类在重写父类方法的时候必须传入对应的参数,如果抽象父类的抽象方法中有返回值,那么子类在重写这个抽象方法的时候,也必须要传入返回值;
public abstract class Animal
{
public virtual void TestVirtual()
{
Console.Writeline("都有生命");
} //抽象类中可以有虚方法,子类不一定要将它重写;
public abstract void Bark();
public abstract string Name
{
get;
set;
} //抽象属性;
public abstract string StringTest(string name);
private int _age;
public int Age
{
get{return _age;}
set{_age = value;}
}
public Animal(int age)
{
this.Age = age;
}
public Animal()
{
}
}
public class Dog : Animal
{
public override void bark()
{
Console.WriteLine("狗狗叫");
}
public override string StringTest(string name);
{
throw new NotImplementedException();
}
public override string Name
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException(); //实现抽象属性,在:Animal左下角有小蓝线,alt+shift+F10点击即可;
}
}
}
public class Cat : Animal
{
public override void bark()
{
Console.WriteLine("猫咪叫");
}
public override string Name
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
}
public abstract class Test:Animal
{
//如果子类也是抽象类,那么不用重写父类的抽象成员;
}
如果父类的方法有默认的实现,并且父类需要被实例化,这时可以考虑将父类定义成一个普通类,用虚方法来实现多态;如果父类中的方法没有默认的实现,父类也不需要被实例化,则可以将该类定义为抽象类;
在虚方法当中,如果说这个方法有方法体的话,我们可以考虑用虚方法来实现它,我有一个抽象类,抽象类中有一个函数,这个函数本来是有方法体的,但是我不给他写方法体;既然你不给他写方法体,你就应该用抽象类来实现,你写了方法体,就应该用虚方法来实现,
抽象类当中同样可以写一个虚方法,子类可以重写它,虚方法一般父类也可以去调用,但是在抽象类中抽象类调用不了这个虚方法;
猫和狗都有生命;这下父类的方法有方法体了,但是这时候就没有必要用抽象类去做这么一件事了,要实现这样一个需求的话,不需要用到抽象类,一个普通类就可以搞定了,
当然,也不是说在抽象类中写虚方法没有意义,如果要同时实现狗和猫叫(抽象类),狗和猫都有生命(虚方法)这两个事,这时候既得写一个抽象方法又得写一个虚方法;在抽象类中的虚方法,子类依然可以去重写它;
结论,如果类里面需要抽象的成员,你就写抽象类,如果不需要抽象成员,就不写抽象类,普通类就可以了,
自我小结:如果只需要实现狗和猫有生命,那么只要写虚方法就可以了;父类是动物类,一个普通类;
如果只需要实现狗和猫都会叫,那么需要写一个抽象类的父类animal;
如果既要实现狗和猫有生命,又要实现狗和猫都会叫,那么要写一个抽象类animal,在这个抽象类里面要写虚方法有生命;
使用多态求矩形的面积和周长,以及圆的面积和周长
public abstract class Shape
{
public abstract double GetArea();
public abstract double GetPerimeter();
}
public class Circle:Shape
{
private double _r;
public double R
{
get{return _r;}
set{_r = value;}
}
public Circle(r)
{
this.R =r;
}
public override double GetArea()
{
return Math.Pi * this.R * this.R;
}
.......................
}
Shape s = new Circle(5);
double area = s.GetArea();
静态类和抽象类都不能创建实例,但是抽象类能创建指向子类对象的实例;
用了多态的感受,一个对象能够表现不同的类型,简化了很多代码;
//用多态来实现将移动硬盘,U盘,MP3插到电脑上进行读写数据;需要画图设计类;
将可移动存储设备作为移动硬盘,U盘,MP3的父类; 继承的思想
电脑有两个方法CpuRead,CpuWrite两个方法,调用各个设备的read()和write()方法;
电脑虽然不知道是谁插进来,但是按照父类去处理肯定错不了(已经有点多态的思想了);
1.电脑类当中要用到可移动存储设备类的对象,传参传进来就好了,
public void CpuRead(MobileStorage ms)
{
ms.Read();
}
MobileStorage ms = new Mp3 //new UDisk //new MobileDisk
Computer cpu = new Computer();
cpu.CpuRead(ms);
2.可以用字段
private MobileStorage _ms;
public MobileStorage Ms
{
get{return _ms;}
set{_ms = value;}
}
public void CpuRead()
{
Ms.Read();
}
MobileStorage ms = new Mp3 //new UDisk //new MobileDisk
Computer cpu = new Computer();
cpu.Ms = ms;
cpu.CpuRead();
3.也可以用构造函数;
List<T>比ArrayList的好处就是不会发生拆装箱;Dictionary比HashTable也是如此
虚方法是方法,抽象类是类;
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(四):结合BotSharp
· 软件产品开发中常见的10个问题及处理方法
· Vite CVE-2025-30208 安全漏洞
· 《HelloGitHub》第 108 期
· MQ 如何保证数据一致性?