一、复习
1.什么是接口?说说你对接口的理解。
(提示:概念、语法、应用场景,与抽象类的区别。说出最特别的)
接口是一种规范、标准,一种抽象的概念,所以本身无法实现,定义时用interface,
只能有方法(属性、方法、索引器等)且不能有方法体。不能有字段等数据,且定义
时不能用访问修饰符(隐式为public)。接口必须在继承中实现(除非是抽象方法),
可以多继承。
接口与抽象类都用于多态。都必须在继承中实现。但抽象类主要是同类中且只
只能单继承。但接口可以不同类中实现多态(表示一种行为特征,如行为、能力等),
且可以多继承。多个继承时,类名写在最前,后面接多个接口。如果类中有与接口
同名的方法,可以使用显式定义接口(无修饰符)。
结构也可以实现接口,但结构不能继承。
2、说说try-catch的注意点?
(提示:语法结构,finally,返回值)
try-catch-finally,其中catch与finally必须至少有一个与try配对。try块中有异常
则其后续代码不再执行,跳到catch中执行。catch可以有多个,只要有一个满足条件
其它就不执行了。有finally时无论异常否必须执行,且在try与catch中可以有return,
但finally中不能有return,返回前也必须执行finally.
另外因为有返回值的方法都会创造一个临时变量存储返回值,因此当把return
写入try或catch中时,小心返回值的变化。
3、静态方法能被重载或重写吗?
答:静态方法当类加载时就被加载到内存中,一直保持不变,直到程序退出。所以它
是不能被重写的。
而非静态方法是在对象实例化时,才单独申请内存空间,为每一个实例分配独立
的运行内存,因而可以重写。
静态方法与非静态方法都可以被重载,因为重载是在编译器编译时发生,与运行
时无关。
简言之:重载是编译时多态,重写是运行时多态。
引申前面的:object.ReferenceEquals()不能被重写或重载。
因为这是静态方法,所以不能被重写。另外不能人为进入object中去修改源代码进行
重载(这是微软的内部领域)
二、常用类库String
学习.net就是学习它的无数个类库怎么用.
1、字符串的一些特性:
string类型不能被继承(sealed关键字):密封类
(1)sealed有两种用法:
1)类名前加sealed,表示此类不能向下继承。
2)方法重写时添加sealed,表示此方法不再向下进行重写。到此为止。
internal class Program
{
private static void Main(string[] args)
{
Person p = new C2();
p.SayHi();
Console.ReadKey();
}
}
internal class Person
{
public virtual void SayHi()
{
Console.WriteLine("基类");
}
}
internal class C1 : Person
{
public override sealed void SayHi()
{
Console.WriteLine("子类1");
}
}
internal class C2 : C1
{
public override void SayHi()//出错,因为前面已经sealed,不能向下再重写.
{
Console.WriteLine("孙类2");
}
}
思考:
为什么string类型不允许继承(sealed)?(后面答案)
(2)不可变性 (ToUpper演示,查看string类代码,只读索引器),
字符串的任一方法返回的都是一个新的副本,其本身未变,因为它是不可变的。
private static void Main(string[] args)
{
string s1 = "Hello World";
string s2 = s1.ToUpper();
Console.WriteLine(s1 + "---" + s2);//本身未变
Console.ReadKey();
}
结论:
字符串一旦被创建,永远不可更改。
private static void Main(string[] args)
{
string s1 = "abc";
string s2 = "x";
s1 = s1 + s2;
Console.WriteLine(s1 + "---" + s2);
Console.ReadKey();
}
注意:
s1发生变化。原因:堆中原"abc"未变。堆中新创建了"abcx",其地址赋值
给了栈中的s1,所以s1指向了新的堆中地址。(原abc无人指向,但不会被回收)
private static void Main(string[] args)
{
string a = "a";
string b = "b";
string c = "c";
a = a + b;
a = a + c;
Console.WriteLine(a);
Console.ReadKey();
}
上面在堆中开辟了5块空间,也就是创建了5个对象。
下面输出的结果相同,且开辟的空间和对象也是5个。
private static void Main(string[] args)
{
string a = "a";
a = a + "b";
a=a+"c";
Console.WriteLine(a);
Console.ReadKey();
}
可以看到字符串的拼接,造成了大量的内存浪费,每次创建字符串都会浪费资源。
这都是字符串的不可变性造成的资源浪费。
(3)字符串暂存池(拘留池)
"abc",与控制台输入的"abc",与"a"+"b"+"c",与三个变量abe相加是否为同一
个对象?(以此说明只针对常量。)
string s1 = new string(new char[] { 'a', 'b', 'c' });
string s2 = new string(new char[] { 'a', 'b', 'c' });
s1 = s2;
s1与s2是同一个对象.
查看下面两种情况反编译情况:变量连接情况:
private static void Main(string[] args)
{
string s1= "abc";
string a = "a", b = "b", c = "c";
string s2 = a + b + c;
Console.WriteLine(s2);
Console.ReadKey();
}
反编译如
因此可以看出最后是三个字符串Concat,因此是新创建变量,s1与s2是不同的。
private static void Main(string[] args)
{
string s1 = "abc";
string a = "a", b = "b", c = "c";
string s2 = "a" + "b" + "c";
Console.WriteLine(s2);
Console.ReadKey();
}
反编译后如:
因此,看出实际,就是直接指向前面的s1,未创建新的变量,s1与s2是相同的。
原因:
针对字符串常量,建立一个缓存池,把常量字符串,建立一张在堆中的索引
表。内部维护一个哈希表:key为字符串,value是地址。每次为一个新变量赋值
都会找key中是否有,如果有则直接把value中的地址赋值给新变量。
这样只要有,就直接返回地址,便于快速创建提高效率。
依赖于字符串的"不可变性",在整个程序的生命期中,该拘留池并不会消失
也不会发生变量,只会在程序退出后释放,所以它会一直霸占堆中内存。
1]、为什么只常量,不是变量?
因为它的霸占,只适用于少量多次使用的情况。如果再加入众多的变量情况
会造成堆中大量内存被占用,拖累程序或使程序崩溃。
2]、为什么要有字符串暂存池?
暂存池(拘留池)如同缓存一样,建立表后,可以有效提高命中率,节省创
建字符串对象的时间。
3]、大量的字符串常量,如何避免进入拘留池?
由于其不可变性,若用大量的字符串常量,会造成堆中内存无效浪费。这
时可以尽量用变量来使用字符串,使其在变量生命期外自动释放。
例如:2万的字符串常量,可以存储在文件中,用变量进行读取,当这个
文件关闭后,就可以释放该变量,而不用拘留池学期驻留在堆内存中。
(4)如何手动添加到拘留池?
对于动态字符串本身在哈希表中没有,通过这种Intern可以添加到该哈希表中,
目的为了提高性能。
String.Intern(x): Intern方法使用暂存池来搜索与 str 值相等的字符串。如果存
在这样的字符串,则返回暂存池中它的引用。如果不存在,则向
暂存池添加对 str 的引用,然后返回该引用。
String.lsintened(x): 此方法在暂存池中查找 str。如果已经将 str放入暂存池中
则返回对此实例的引用;否则返回nullNothingnullptrnull引用
注意:
可以手动添加到拘留池,但不能手动在拘留池中删除。
(5)为什么字符串要添加sealed,即不允许继承?
答:两个原因:
1、子类若能继承,可能会对字符串类进行修改(改变字符串的特性如不可变性);
2、CLR对字符串提供了各种特殊的操作,如果有很多类继承了字符串类,则CLR
需要对更多的类型提供特殊操作。这样有可能降低性能。
比如,不可变性,下面的众多子类也会如此处理。
三、字符串格式化
1、格式化
演示: string.Format("===0,30:c31====",3.1415926535384);
说明:{索引[,对齐][:格式字符串]}
索引: 表示引用的对象列表中的第n个对象参数
对齐(可选): 设置宽度+对齐方式,该参数为带符号的整数。
正数为右对齐(不能加正号),负数为左对齐。
例如: {0,50}表示宽度为50,右对齐。{0,-50}表示宽度为50,左对齐。
格式字符串:常见的有"数字格式"、"日期与时间格式"、自定义格式等。
用"冒号"开始。例如,
数字格式: C表示货币、D表示十进制数、E表示科学记数法、G表示常规...
其中可以写成Cxx.xx表示精度,范围为0-99,例如c3表示保留3位小数。
日期时间格式: d表示短日期,D表示长日期,f表示完整日期+短时间,F表
示完整日期+长时间,t表示短时间模式...
private static void Main(string[] args)
{
string s = "abc";
Console.WriteLine("01234567890123456789------");
Console.WriteLine(s);
Console.WriteLine("=={0,10}==", s);
Console.WriteLine("=={0,-10}==", s);
double n = 3.14567;
Console.WriteLine();
Console.WriteLine("01234567890123456789------");
Console.WriteLine(n);
Console.WriteLine("=={0,10:f2}==", n);
Console.WriteLine("=={0,10:e2}==", n);
DateTime d = DateTime.Now;
Console.WriteLine();
Console.WriteLine("01234567890123456789------");
Console.WriteLine(d.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine(d.ToString("d"));
Console.WriteLine(d.ToString("D"));
Console.WriteLine(d.ToString("f"));
Console.WriteLine(d.ToString("F"));
Console.ReadKey();
}
注意:
Console.WriteLine()没有返回值直接输出到控制台
string.Format()有返回值,可以赋值给一个字符串。
2、String字符串常用方法
字符串可以看成字符数组,不可变特性
(通过for循环,修改string中的元素,将失败! 提示为只读属性)。
当输入时就会提示错误:
用net Reflector查询一下string类中的索引器char[]的反编译情况:
可以看到属性只有get,没有set,即只能读取,不能写入。
属性
Length //获得字符串中字符的个数。"aA我你他"->5
方法
IsNullOrEmpty() 静态方法判断为null或者为""(静态方法)
ToCharArray() 将string转换为char[]
ToLower() 小写,必须接收返回值。(因为: 字符串的不可变)
ToUpper() 大写。
Equals() 比较两个字符串是否相同。忽略大小写的比较,StringComparation.
IndexOf() 如果没有找到对应的数据,返回-1.
LastlndexOf() 如果没有找到对应的数据,返回-1
Substring() 2个重载,截取字符串。
Split() 分害字符串。
Join() 静态方法
Format 静态方法
Replace()
怎么判断一个字符串是空字符串?
null,空对象。堆中没有分配内存,变量在栈中存储的内容为0x000000,
即不指向堆中的任何地址。
"",空字串。堆中已经分配了内存,只是内容为空。变量在栈中存储的内容就
是在堆中分配内存的地址。
空串的判断:
private static void Main(string[] args)
{
string s = "";
if (s == "")//方法一
{
}
if (s == string.Empty)//方法二
{
}
if (s.Length == 0)//方法三 推荐
{
}
Console.ReadKey();
}
或者可能为null时:
private static void Main(string[] args)
{
string s = "";
if (s == null || s.Length==0)//方法一
{
}
if (string.IsNullOrEmpty(s))//方法二
{
}
Console.ReadKey();
}
对两个字符串比较是否相同,忽略大小写。
private static void Main(string[] args)
{
string s1 = "abc";
string s2 = "ABC";
Console.WriteLine(s2.ToLower() == s1.ToLower());
Console.WriteLine(s1.ToLower().Equals(s2.ToLower()));
Console.WriteLine(s1.Equals(s2, StringComparison.OrdinalIgnoreCase));
Console.ReadKey();
}
面试题: 统计一个字符串中,"天安门"出现的次数。
private static void Main(string[] args)
{
string s = "天安门中数天安门,天安门中有几个天安门?";
int count = 0, i = 0;
while (true)
{
i = s.IndexOf("天安门", i);
if (i == -1 || i > s.Length - 3)//未找到或已超过长度
{
break;
}
i += 3;//词的长度
count++;//累加
}
Console.WriteLine(count);
Console.ReadKey();
}
如何把char[]数组转为字符串string?
不能直接用ToString,没有这个重载,只会输出类型。
private static void Main(string[] args)
{
char[] chars = new char[] { 'a', 'b', 'c' };
string s1 = chars.ToString();
Console.WriteLine(s1);//System.Char[]
string s2 = new string(chars);
Console.WriteLine(s2);//"abc"
Console.ReadKey();
}
截取字符串。(从零开始计数)
string s = "welcome to our country!!!";
Console.WriteLine(s.Substring(11, 1));//our
四、字符串的处理练习
1、接收用户输入的字符串,将其中的字符以与输入相反的顺序输出。"abc"->"cba".
private static void Main(string[] args)
{
Console.WriteLine("请输入字符串:");
string s = Console.ReadLine();
if (s.Length > 1)
{
char[] c = s.ToCharArray();
for (int i = 0; i < s.Length / 2; i++)
{
char temp = c[i];
c[i] = c[s.Length - 1 - i];
c[s.Length - 1 - i] = temp;
}
s = new string(c);
}
Console.WriteLine(s);
Console.ReadKey();
}
2、接收用户输入的一句英文,将其中的单词以反序输出。
"I love you""l evol uoy"
private static void Main(string[] args)
{
Console.WriteLine("请输入一句英文:");
string s = Console.ReadLine();
s = s.Replace("\"", " \" ").Replace("?", " ? ").Replace(".", " . ").Replace(",", " , ");
string[] s1 = s.Split(' ');
for (int i = 0; i < s1.Length; i++)
{
s1[i] = Reverse(s1[i]);
}
s = string.Join(" ", s1).Replace(" \" ", "\"").Replace(" ? ", "?").Replace(" . ", ".").Replace(" , ", ",");
Console.WriteLine(s);
Console.ReadKey();
}
private static string Reverse(string s)
{
if (s.Length > 1)
{
char[] c = s.ToCharArray();
for (int i = 0; i < s.Length / 2; i++)
{
char temp = c[i];
c[i] = c[s.Length - 1 - i];
c[s.Length - 1 - i] = temp;
}
s = new string(c);
}
return s;
}
结果:
3、"2012年12月21日"从日期字符串中把年月日分别取出来,打印到控制台.
private static void Main(string[] args)
{
string s = "22012年1月1日";
int i = 0, j = s.IndexOf("年");
string year = s.Substring(0, j);
i = s.IndexOf("月");
string month = s.Substring(j + 1, i - j - 1);
j = s.IndexOf("日");
string day = s.Substring(i + 1, j - i - 1);
Console.WriteLine(year + "-" + month + "-" + day);
Console.ReadKey();
}
4、把csv文件中的联系人姓名和电话显示出来。简单模拟csv文件,csv文件就是使用
分割数据的文本,输出:姓名: 张三 电话: 15001111113
string[] lines = File.ReadAllLines("1.csv",Encoding.Default);
//读取文件中的所有行,到数组中。
private static void Main(string[] args)
{
string[] s = File.ReadAllLines(@"E:\csv.csv");
for (int i = 0; i < s.Length; i++)
{
string[] p = s[i].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine($"姓名:{p[0]},电话:{p[1]}");
}
Console.ReadKey();
}
5、123-456--7--89---123--2把类似的字符串中重复符号"-"去掉,即得到
123-456-7-89-123-2.
split(),StringSplitOptions.RemoveEmptyEntries,Join()
private static void Main(string[] args)
{
string s = "123-456--7--89---123--2";
string[] p = s.Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine(string.Join("-", p));
Console.ReadKey();
}
6、从文件路径中提取出文件名(包含后缀)。比如从c:\a\b.txt中提取出b.txt这个
文件名出来。以后还会学更简单的方式:"正则表达式",项目中我们用微软提供
的:Path.GetFileName();(更简单)
private static void Main(string[] args)
{
string s = @"c:\a\b.txt";
string s1 = Path.GetFileName(s);
string s2 = Path.GetDirectoryName(s);
Console.WriteLine(s1);
Console.WriteLine(s2);
Console.ReadKey();
}
7、"192.168.10.5[port=21,type=ftp]",这个字符串表示IP地址为192.168.10.5的服务
器的21端口提供的是ftp服务,其中如果",type=ftp"部分被省略,则默认为http服
务。请用程序解析此字符串,然后打印出"IP地址为***的服务器的***端口提供的服
务为***"
line.Contains("type=")。192.168.10.5[port=21]
private static void Main(string[] args)
{
string s = "192.168.10.5[port=21,type=ftp]";
string[] p = s.Split(new string[] { "[port=", ",type=", "]" }, StringSplitOptions.RemoveEmptyEntries);
string type;
if (p.Length == 2)
{
type = "http";
}
else
{
type = p[2];
}
Console.WriteLine($"IP地址为{p[0]}的服务器的{p[1]}端口提供的服务为{type}");
Console.ReadKey();
}
8、求员工工资文件中,员工的最高工资、最低工资、平均工资
工资文件1.txt:
张三=5000
李四=6000
王五=7000
赵六=8000
田七=5500
孙八=7100
private static void Main(string[] args)
{
string[] s = File.ReadAllLines(@"E:\1.txt");
double[] ds = new double[s.Length];
for (int i = 0; i < s.Length; i++)
{
ds[i] = Convert.ToDouble(s[i].Substring(s[i].IndexOf("=") + 1));
}
Console.WriteLine($"最大值{ds.Max()},最小值{ds.Min()},平均值{Math.Round(ds.Average(), 2)}");
Console.ReadKey();
}
9、"北京传智播客软件培训,传智播客.net培训,传智播客Java培训。传智播客官网:
http://www.itcast.cn。北京传智播客欢迎您。"。在以上字符串中请统计出
"传智播客"出现的次数。找IndexOf()的重载
private static void Main(string[] args)
{
string s = "北京传智播客软件培训,传智播客.net培训,传智播客Java培"+
"训。传智播客官网:http://www.itcast.cn。北京传智播客欢迎您。";
int count = 0, i = 0;
List<int> list = new List<int>();
while (true)
{
i = s.IndexOf("传智播客", i);
if (i == -1 || i > s.Length - 4)
{
break;
}
list.Add(i);//索引位置
i += 4;
count++;
}
Console.WriteLine(count);
Console.ReadKey();
}
private static void Main(string[] args)
{
string s = "北京传智播客软件培训,传智播客.net培训,传智播客Java培" +
"训。传智播客官网:http://www.itcast.cn。北京传智播客欢迎您。";
int count = 0, index = 0;
while ((index = s.IndexOf("传智播客", index)) != -1)
{
count++;
index += 4;
}
Console.WriteLine(count);
Console.ReadKey();
}
五、常用类库StringBuilder
StringBuilder高效的字符串操作
当大量进行字符串操作的时候,比如,很多次的字符串的拼接操作。
String 对象是不可变的。每次使用 System.String 类中的一个方法时,都要
在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。
在需要对字符串执行重复修改的情况下,与创建新的 String 对象相关的
系统开销可能会非常大。
如果要修改字符串而不创建新的对象,则可以使用System.Text.StringBuilder 类。
例如,当在一个循环中将许多字符串连接在一起时,
使用StringBuilder 类可以提升性能。
StringBuilder != String//将StringBuilder转换为 String.用ToString().
StringBuilder仅仅是拼接字符串的工具,大多数情况下还需要把StringBuilder转换
为 String.
StringBuilder sb = new StringBuilder().
sb.Append();//追加字符串
sb.ToString();//把StringBuilder转换为字符串。
sb.Insert();
sb.Replace():
六、垃圾回收
1、CLR的一个核心功能一垃圾回收。
2、垃圾回收的目的:
提高内存利用率。
(内存是有限的,程序中断地使用,若分配的变量都不回收,内存会越来越小,当
不够用时,程序就会崩溃。)
3、垃圾回收器,它回收什么?
只回收托管堆中的内存资源,
不回收其他资源(数据库连接、文件句柄、网络端口等)。
栈中的值类型资源,不需要垃圾回收。它们使用完成后会立即释放。
4、什么样的对象才会被垃圾回收?
没有变量引用的对象。没有变量引用的对象,表示可以被回收了(null)断了线的
风筝,再也回不来了。
大学食堂(自己收盘子)、大排档(不需要程序员自己收盘子)
可以被回收,并不意味着它马上就被回收。
问题1:下面运行到x处时p分配的空间是否可以被回收?
internal class Program
{
private static void Main(string[] args)
{
Person p = new Person();
p.Name = "杨贵妃";
Person p1 = p;
p = null;
//p1=null;
Console.ReadKey();//x处
}
}
internal class Person
{
public string Name { get; set; }
}
答:不可以。p虽然为空,但已经转交给p1,由p1指向该空间,该空间仍然在被p1
使用。如果下一句有p1=null,这块内存再也不会有任何对象引用。那么这块
内存如断线的风筝,再也没人引用指向。这时候是可以被回收。
注意:可以被回收并不意味着马上被回收。
什么时候回收,时间并不确定,这由GC决定。
问题2:下面运行到x处时,p1,p2,p3是否可以被回收?
internal class Program
{
private static void Main(string[] args)
{
Person p1 = new Person();
Person p2 = new Person();
Person p3 = new Person();
Person[] ps = new Person[] { p1, p2, p3 };
p1 = null;
p2 = null;
p3 = null;
Console.ReadKey();//x
}
}
internal class Person
{
public string Name { get; set; }
}
答:不可以。p1,p2,p3解除了引用,但ps仍然在引用这块内存。
5、垃圾回收GC在什么时间回收?
不确定,当程序需要新内存的时候开始执行回收。
Gc.Collect();
//手动调用垃圾回收器。不建议使用,垃圾回收时会暂停一下(非常短暂)
//让程序自动去GC。
垃圾回收是.Net的CLR自动来执行,一般不需手动干预。
正如食堂吃完后,无需收拾由阿姨来自动收拾。但你吃完非要喊一句"阿姨来收拾"
这反而打乱了程序的自动回收流程,造成效率低下。
什么时候必须进行手动回收垃圾呢?
如果在即将进行了一大段非常重要的代码运行,并且预估可能使用大量的内
存,不需要或者尽量不让垃圾回收,这时可以提前使用手动调用垃圾回收。可以
释放出一些较多空余的内存,同时手动回收后,在这段代码运行期间再次被自动
垃圾回收的机率就会变小,从而不会影响这段代码的效率。
另外就是非托管的程序代码,它已经不在GC控制范围,所以必须手动显式地
进行回收,否则会占用系统资源,甚至可能出现意想不到的错误。
6、垃圾回收对程序有影响吗?
垃圾回收时,程序会暂停,保存当前的运行状态,然后进行清理,清理结束后,
恢复原来的运行状态,程序继续进行。
所以垃圾回收是对程序有一些影响的,但这个影响已经优化到极限,一般
不影响程序,人也无法感受到。除非对一些性能有苛刻要求的程序才会有影响。
7、垃圾回收的过程是怎样的?
如果垃圾回收每次都把整个堆全面搜索一次进行清理,那么它的效率明显就很
低,还影响程序的运行。
于是垃圾回收器使用"代"的概念,采用"代"的机制进行回收,大大提高了垃
圾回收的效率。
一共分3代: 第0代、第1代、第2代。
private static void Main(string[] args)
{
int n = GC.MaxGeneration;//垃圾回收支持的最大代数
Console.WriteLine(n);//2 即0,1,2共三代
GC.Collect(0);//只回收第0代
GC.Collect(1);
GC.Collect(2);
GC.Collect();//回收所有代数
Console.ReadKey();
}
各代的回收频率:第0代最高,其次第1代,再次第2代。
每个代的都指定了一定的初始容量空间,来装载各代变量。
申请变量首先进入第0代,直到占满第0代的初始规定容量时:
先回收第0代,第0代回收后存活下来的,就进行第1代。如此往复,直到第0代
存活下来的,试图再进入第1代时,发现第1代初始容量空间已经满了,则回收第1
代。同时第1代回收后存活的,则进入第2代。
如此重复,也就是说越老的对象生存几率越大,而新建的对象则最易被回收。
第0代的就如士兵,炮灰,最易被回收;第0代的士兵存活下来的,去第1代当
团长。一旦第1代的团长也满后,才去从回收第1代,从团长群中找存活的,进入
第2代去当军长。
如果第0-2代都满了,则会试着扩大0-2代各代的初始容量,以适应程序运行。
如果一直扩大,都无法满足程序,则到达一限度后,程序就会抛出异常。
8、.net中垃圾回收算法:
mark-and-compact(标记和压缩),一开始假设所有对象都是垃圾。
首先确定所有可达对象,然后移动这些对象,使它们紧挨着存放,有点类似于
磁盘碎片整理。
一次垃圾回收周期开始时,垃圾回收器会识别出对象的所有根引用,然后遍历
根引用所标识的树形结构,并递归确定所有根引用指向的对象,这样,垃圾回收器
就识别出了所有可达对象。
执行垃圾回收时,垃圾回收器将所有可达对象紧挨着放在一起,从而覆盖不可
达对象所占用的内存。
图中上部分是回收前(绿色在用,黄色为垃圾),回收后是下半部分。
此算法的优点是不会导致内存碎片化,回收后内存分配更高效。
缺点是:移动内存需要额外的开销,同时仍然要扫描整个内存空间。
所以为了优化使用代的概念,避免扫描整个内存。
9、除了内存资源外的其他资源怎么办?
比如a方法中调用了系统中的方法。
由于这些资源是非托管的,所以需要手动去释放。比如析构函数,或者Dispose()等。
10、为什么.net没有析构函数?
析构函数的目的是释放资源,但已经有了GC,它会自动调用GC进行资源的释放。
但,也可以人为写一下象C++中的"析构函数":
internal class Program
{
private static void Main(string[] args)
{
}
}
internal class Person
{
~Person()
{
}
}
上面生成后,用.Net Reflector反编译看一下:
可以看到这个"析构函数",实际上被编译成终结器(终结函数,也即"析构函数")
protected override void Finalize();
当对象可以被回收,当GC回收时,它先执行这个终结器,再调用GC进行回收。所
以一般非托管资源的释放,就放在Finalize()这个终结器中,以便克服GC不能回
收非托管资源的缺陷。
但这个有一个缺点:
尽管可以被回收,但并不能马上被回收,这个要等GC来回收时,才先执行这
个终结器,造成非托管资资源一直被占用。
简言之:程序员不能控制析构器何时将被执行因为这是由垃圾收集器决定的。
为了克服这个缺点,使用一个叫IDispose的接口,它是所有处理所有非托管
资源的释放方法,需要人为在对象中重新实现,可以直接手动调用马上释放。而
不必用析构函数或终结器(它需要等待GC的到来)。
internal class Program
{
private static void Main(string[] args)
{
Person p = new Person();
p.Name = "晋文公";
p.Dispose();//手动立即释放,不必等GC
Console.ReadKey();
}
}
internal class Person : IDisposable
{
public string Name { get; set; }
//~Person()//不推荐,不使用
//{
//}
public void Dispose()
{
//这里人为手动实现,在不用对象时,直接调用该方法可立即释放
Console.WriteLine("Dispose()");
}
}
上面代码另一个有效释放就是用using语句,主程序改为:
private static void Main(string[] args)
{
using (Person p = new Person())
{
p.Name = "KO";
}
Console.ReadKey();
}
using()是使用非托管资源的最佳方式,可以确保资源在代码块结束之后被正确释放,
并且代码更简洁。
这里说的非托管资源指的是实现IDisposable或IAsyncDisposable接口的类。
using实际上是一个try-finally,在finally会自动调用dipose,即无论执行正常与
否,最终都会有非托管资源的释放。
当代码离开using语句块,就会自动调用声明的非托管变量中dispose.
七、弱引用
当一个对象在堆中没有任何人引用时,它就是一个断线的风筝,它就可以被垃圾回收。
但有时我们程序中动态中可能会再次使用它,但又不清楚它是否已经被垃圾回收了。
此时,在这个对象(风筝)断线(null)之前,就设置一个"标志"(弱引用)。
在下次可能再使用前,进行该对象的引用或判断。
1、为什么要使用弱引用?
因为创建一个对象,特别是一个大的对象,特别费资源。弱引用能很大程序
上免除重新创建一个新对象,而是直接使用原来的对象,节省资源。
2、弱引用的注意事项
弱引用类为WeakReference,主要有:
IsAlive:判断是否存活。
注意可能判断时还存活,但下一瞬间就被回收了。
Target: 目标对象(即原对象)。
利用此属性,可将当前对象进行转换。
internal class Program
{
private static void Main(string[] args)
{
Person p = new Person();
p.Name = "朱元彰";
WeakReference wr = new WeakReference(p);//声明弱引用
p = null;
方法一:不推荐
//if (wr.IsAlive)
//{
// object o = wr.Target;
// if (o != null)
// {
// Person p1 = o as Person;
// //p活了(即p1),又可以用了
// }
//}
方法一:啰嗦
//if (wr.Target != null)
//{
// object o = wr.Target;
// Person p1 = o as Person;
// //p1又活了,又可以用了
//}
//方法三:推荐
Person p1 = wr.Target as Person;
if (p1 == null)
{
//重新创建p1
}
else
{
//p活了为p1,又可以使用了。
}
Console.ReadKey();
}
}
internal class Person
{
public string Name { get; set; }
}
方法一:在IsAlive判断后的瞬间,可能被GC回收,所以代码本身就有bug。
方法二:过程比较啰嗦。
方法三:推荐写法。直接提取转换,为null就新建,否则直接使用。
3、弱引用的工作原理
引用https://www.cnblogs.com/johnyang/p/17205466.html
弱引用保持的是一个GC"不可见"的引用,是指弱引用不会增加对象的引用计数,
也不会阻止垃圾回收器对该对象进行回收。因此,弱引用的目标对象可以被垃圾回
收器回收,而弱引用本身不会对垃圾回收造成任何影响。
弱引用的原理是,在堆上分配的每个对象都有一个头部信息,用于存储对象的
类型信息、对象的大小等信息。在头部信息中,还会有一个标志位用于表示对象是
否被引用。
当一个对象被创建时,该标志位为"未引用"。当该对象被弱引用引用时,该标
志位不会变为"已引用",即该对象仍然会被当做未引用的对象进行处理。
被强引用后,会被标记为"已引用",当所有的强引用都消失时,该标志位会变
为"未引用",即该对象已经没有任何强引用指向它,标记的工作由GC来完成。
在垃圾回收时,GC会根据标记-清除算法对堆中的对象进行扫描和标记,标记
所有仍然被引用的对象,然后回收所有未被标记的对象。
对于被弱引用引用的对象,由于弱引用不会增加对象的引用计数,也不会阻止
垃圾回收器回收该对象,因此在回收时,该对象会被当做未被引用的对象进行处
理,然后被回收。 总之,弱引用保持的是一个GC"不可见"的引用,即弱引用不会影响垃圾回收器
对目标对象的回收,因此可以用于实现一些场景,例如缓存、对象池等场景,避免
长时间占用内存或造成内存泄漏。
private static void Main(string[] args)
{
var sb = new StringBuilder("weak");
var weak = new WeakReference(sb);
Console.WriteLine("before GC");
Console.WriteLine(weak.Target);
GC.Collect();
Console.WriteLine("after GC");
if (weak.Target == null)
{
Console.WriteLine("now it has been cleared...");
}
else
{
Console.WriteLine(weak.Target);
}
Console.ReadKey();
}
在vs2022上工具栏下选择Debug/Release两种模式分别生成可执行文件。在对应的
项目的子目录中的Debug或Release中分别生成exe文件,分别执行那么结果不同:
1)Debug下:
before GC
weak
after GC
weak
2)Release下:
before GC
weak
after GC
now it has been cleared...
在 debug 模式下,GC.Collect 方法仍然会工作,但是它的行为可能会受到
一些影响。
在 debug 模式下,编译器会添加额外的调试信息到代码中,这些信息可能会
影响垃圾回收器的行为。例如,编译器可能会保留一些对象的引用,以便调试器
可以访问它们,这可能会导致这些对象不会被垃圾回收器回收,直到调试器不再
需要它们为止。
因此,当调用 GC.Collect 方法时,由于存在调试信息的影响,可能会出现
一些对象无法被立即回收的情况。
此外,在 debug 模式下,垃圾回收器的性能也可能会受到一定的影响,因
为编译器会添加额外的代码和调试信息,导致程序变得更加复杂和庞大,从而使
垃圾回收器需要更长的时间来扫描和回收对象。
因此,如果需要在 debug 模式下进行垃圾回收操作,应该仔细考虑其影响,
并进行充分的测试,以确保程序的正确性和性能。
同时,还可以考虑使用其他的调试工具和技术来诊断和解决问题,避免对
程序的垃圾回收行为产生不必要的影响。