(05)基础强化:字符串拘留池,格式化,StringBuilder,垃圾回收,弱引用

    
    
    
一、复习


    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 模式下进行垃圾回收操作,应该仔细考虑其影响,
        并进行充分的测试,以确保程序的正确性和性能。
            同时,还可以考虑使用其他的调试工具和技术来诊断和解决问题,避免对
        程序的垃圾回收行为产生不必要的影响。
        
        
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值