• C#6.0,C#7.0新特性


    C#6.0,C#7.0新特性 

    C#6.0新特性

        • Auto-Property enhancements(自动属性增强)
          • Read-only auto-properties (真正的只读属性)
          • Auto-Property Initializers (自动属性的初始化)
        • Expression-bodied function members (表达式方法体)
        • using static (导入类静态方法)
        • Null-conditional operators (一元空值检查操作符?.)
        • String Interpolation (字符串插值)
        • nameof Expressions (nameof 表达式)
        • Index Initializers(索引初始化器)
        • Exception Filters (异常过滤器)
        • Await in Catch and Finally blocks (Catch,Finally语句块中可用await)
        • Extension Add methods in collection initializers (在集合初始化器中使用扩展的Add方法)
        • Improved overload resolution (改进的重载解析)
    • C#7.0新特性
        • out variables (out 变量)
        • Tuples (元组)
        • Discards (占位符)
        • Pattern matching (模式匹配)
        • Ref locals and returns (ref局部变量和返回ref变量)
        • Local functions (本地方法)
        • More expression-bodied members(更多的 表达式方法体 成员)
        • Throw expressions (异常表达式)
        • Generalized async return types(更泛化的异步返回类型)
        • Numeric literal syntax improvements(数值字面量语法改进)
    • C#7.1新特性
        • Async main (异步Main方法)
        • Default literal expressions (default字面量表达式)
        • Inferred tuple element names(tuple元素名可推导)
        • Reference assembly generation
    • C#7.2
        • Reference semantics with value types(只读引用)
        • Non-trailing named arguments(命名参数不需要在最后)
        • Leading underscores in numeric literals(数字字面量的前导分隔符)
        • private protected access modifier (private protected 访问修饰符)

    C#6.0新特性


    Auto-Property enhancements(自动属性增强)

    Read-only auto-properties (真正的只读属性)

    // 以前
    // 只是限制了属性在类外部只读,而在类内部任何地方都可设置
    public string Name { get; private set; }
    public void SetName(string name) {
        Name = name;
    }
    
    // c#6.0
    // 1.通过只使用一个getter来声明真正只读
    // 2.这样的属性,只能在构造器中初始化(含属性声明时),而类内部其他地方也不可再设置
    public string Name { get; } = "小米喂大象"; // 允许
    public User(string Name, string password, int age) {
        Name = name; // 允许
        Password = password;
        Age = age;
    }
    public void SetName(string name) {
        Name = name;  // 报错
    }

    Auto-Property Initializers (自动属性的初始化)

    // 以前
    // 需要属性有setter,通过setter来初始化backing field
    public string Name { get; set; }
    public User(string name) {
        Name = "小米喂大象";
    }
    
    // C#6.0
    // 可以在属性声明的同时初始化
    public string Name { get; } = "小米喂大象";
    public int Age { get; set; } = 18;

    Expression-bodied function members (表达式方法体)

    // C#6.0
    // 1. 类成员方法体是一句话的,都可以改成使用表达式,使用Lambda箭头来表达
    // 2. 只适用于只读属性和方法
    public string Name => "小米喂大象"; // 只读属性
    public void SetAge(int age) => Age = age; // 方法
    public void Log(string msg) => System.Console.WriteLine($"{Name} : {msg}");

    using static (导入类静态方法)

    // c#6.0
    // 1.使用using static 语法,可以将一个类中的所有静态方法导入到当前上下文,
    //   包括这个类中的嵌套类型,不包括实例方法,也不包括const字段
    // 2.这样引用这个类的方法,就可以直接引用,而不用再加前缀(形似C函数)。
    using static System.Math;
    using static System.String;
    
    public Double Calc(int angle) {
        var tmp = Sin(angle) + Cos(ange);
        ...
    }

    Null-conditional operators (一元空值检查操作符?.)

    // 以前
    // 一个引用的null检查和使用是分开的
    User user;
    . . .
    if (user != null) {
        user.SetAge(19);
    }
    
    // C#6.0
    // 1. 直接使用?.代替.操作符即可
    // 2. ?.操作符确保其左边表达式只计算一次
    // 2. 如果引用是null,这直接返回类型匹配的null, 下面的name被推断为string?
    user?.SetAge(19);
    var name = user?.Name;

    String Interpolation (字符串插值)

    // 以前
    public override string ToString() {
        return string.Format("{0}:{1:D2}", Name, Age);
    }
    
    // C#6.0
    // 1. 使用$开头,花括号里直接放入表达式
    // 2. 格式化字符串,可直接在花括号里表达式后面加上:,然后加上格式化字符串
    // 3. 插值表达式里可以嵌套插值表达式
    public override string ToString() {
        return $"{Name}:{Age:D2}";
    }

    nameof Expressions (nameof 表达式)

    // C#6.0
    // 1. nameof表达式返回一个变量、属性或字段的名称
    // 2. 当需要一个符号的名称时很有用,一可以避免手工打错,二可以便于重构
    // 3. 如果是一个限定了前缀的完整名称,如nameof(User.Age),
    //    nameof操作符也只是返回"Age",而不是"User.Age"
    public string Name {
        get => name;
        set {
            name = value;
            PropertyChanged?.Invoke(this, 
                new PropertyChangedEventArgs(nameof(Name)));
        }
    }
    
    public int Age {
        get => age;
        set {
            age = value;
            PropertyChanged?.Invoke(this, 
                new PropertyChangedEventArgs(nameof(User.Age)));
        }
    }

    Index Initializers(索引初始化器)

    // c#6.0
    // 允许使用[]操作符来初始化,这样字典可以像其他序列容器一样的语法初始化了
    public Dictionary<string, User> Users = new Dictionary<string, User> {
        ['小米喂大象'] = new User(),
        ['Jack'] = new User(),
    }

    Exception Filters (异常过滤器)

    // C#6.0
    // 1. catch字句后面可以带一个 when表达式(早期是if,后被when替换)
    // 2. 如果when括号内表达式(下面代码?部分)为真则Catch块就执行,否则不执行
    // 3. 利用这个表达式可以做很多事,包括过滤指定异常、调试、打印日志等。
    try {
    } catch (Excepation e) when (?) {
    }

    Await in Catch and Finally blocks (Catch,Finally语句块中可用await)

    // C#6.0
    // 1. C#5.0中添加了async、await, 但是在哪里放await表达式,这个有一些限制
    // 2. C#6.0中解决了这些限制中的其中一个,就是await可以放在catch、finally语句块中了。
    try {
        var result = await SomeTask;
        return result;
    } catch (Exception e) {
        await Log(e);
    } finally {
        await Cleanup();
    }

    Extension Add methods in collection initializers (在集合初始化器中使用扩展的Add方法)

    // c#6.0
    public class User {
        public string Name { get; set; }
        public int Age { get; set; }
        public User(string name, int age) {
            Name = name;
            Age = age;
        }
    }
    
    public class ActiveUsers : IEnumerable<User> {
        List<User> users = new List<User>();     
    
        //public void Add(User user) {
        //    users.Add(user);
        //}
    
        public void Append(User user) {
            users.Add(user);
        }
    
        public IEnumerator<User> GetEnumerator() {
            throw new NotImplementedException();
        }
    
        IEnumerator IEnumerable.GetEnumerator() {
            throw new NotImplementedException();
        }
    }
    
    // 添加一个扩展的静态方法Add
    public static class ActiveUsersExtensions {
        public static void Add(this ActiveUsers users, User u) 
            => users.Append(u);
    }
    
    public class Test {
        // 1. 为了能够像下面这样使用集合初始化器,ActiveUsers必须拥有一个Add()方法
        // 2. 现在,如果ActiveUsers 类只有一个Append()方法,没有Add()方法,而你也
        //    无法修改这个类,这时候下面的代码就无法工作了。
        // 3. C#6.0中允许你添加一个扩展的静态方法Add来完成工作。
        ActiveUsers users = new ActiveUsers {
            new User("xm01", 18),
            new User("xm02", 19),
            new User("xm03", 20),
        };
    }

    Improved overload resolution (改进的重载解析)

    // C#6.0
    // 1. 下面代码,C#6.0以前,当编译器看到Foo(Bar),会去匹配一个最合适的方法,但是,
    //    编译器最终会报告失败。因为编译器在匹配函数签名的时候,并没有将函数返回值作为
    //    一部分,所以编译器不能明确该调用哪个Foo方法。
    // 2. 同样,C#6.0以前,编译器不能区分Task.Run(Action)和Task.Run(Func<Task>())
    // 3. C#6.0解决了此问题 
    int Bar() { return 1; }
    void Foo(Action f) { }
    void Foo(Func<int> f) { }
    void Main() {
       Foo(Bar);
    }

    C#7.0新特性


    out variables (out 变量)

    // 以前
    // out变量的声明和初始化是分开的
    int age;
    if (int.TryParse("18", out age)) {
        Console.WriteLine("age: " + age);
    }
    
    // C#7.0
    // 1. C#7.0允许直接在方法的调用列表中声明一个out变量
    // 2. 这个变量可以声明成var这种隐式类型
    // 3. 这个变量的作用域范围伸展到if语句块的外部范围
    if (int.TryParse("18", out int age)) {
        Console.WriteLine("age: " + age);
    }
    if (int.TryParse("90", out var score)) { // 隐式类型也可以用
        Console.WriteLine("score: " + score);
    }
    age += 1; // 这时候age仍然有效

    Tuples (元组)

    // c#7.0
    // 1. C#7.0以前就已经有tuple, 但不是语言层面支持的,而且使用起来没效率
    // 2. C#7.0中使用tuple,需要引入 System.ValueTuple(如果平台不包含的话)
    // 3. 元组成员名可指定,不指定默认Item1,Item2,...
    // 4. 元组是值类型,其元素是公开字段,可修改
    // 5. 元组中元素都相等,则元组相等
    // 6. 元组可用于函数返回多个独立变量,这样不用定义一个struct或class
    // 7. 元组使用场合:
    
    // 元组成员名默认Item1, Item2
    var name = ("Jack", "Ma"); 
    (string, string) name1 = ("Jack", "Ma");
    
    // 指定成员名称为firstName, lastName
    (string firstName, string lastName) name2 = ("Jack", "Ma"); 
    
    // 指定成员名称为f, l
    var name3 = (f: "Jack", l: "Ma"); 
    
    // 左右同时指定成员名称,右边的忽略
    (string firstName, string lastName) name4 = (f: "Jack", l: "Ma"); 
    
    // 返回一个元组
    private (string FirstName, string LastName) GetName() {
        return ("Jack", "Ma");
    }
    var name5 = GetName();
    name5.FirstName = "Jack2";
    name5.LastName = "Ma2";
    
    // 析构元组成员到变量firstName, lastName
    (string firstName, string lastName) = GetName(); 
    firstName = "Jack2";
    lastName = "Ma2";
    
    // 析构元组成员到变量f, l
    (string f, string l) = name5;
    f = "Jack2";
    l = "Ma2";
    
    // 析构元组成员到变量f2, l2
    var (f2, l2) = name5;

    Discards (占位符)

    // C#7.0
    // 1. 增加一个占位符_(下划线字符)来表示一个只写的变量,这个变量只能写,不能读。
    //    当想丢弃一个值的时候,可以使用。
    // 2. 他不是实际变量,没有实际存储空间,所以可以多处使用。
    // 3. 一般用于解构元组、调用带out参数的方法、模式匹配,例如:
    //    > 调用一个方法,这个方法带有一个out参数,你根本不使用也不关心这个参数;
    //    > 一个包含多个字段的元组,你只关心其中部分成员,不关心的成员可以使用占位符;
    //    > 模式匹配中, _可以匹配任意表达式;
    // 4. 注意:_也是一个有效的变量标识符,在合理的情景下,_也会作为一个有效变量
    
    private (string FirstName, string LastName) GetName() {
        return ("Jack", "Ma");
    }
    
    private void GetName(out string FirstName, out string LastName) {
        FirstName = "Jack";
        LastName = "Ma";
    }
    
    // 只关心FirstName, LastName丢弃
    var(firstName, _) = GetName();
    GetName(out var firstName2, out _);
    
    // 有效变量_
    public void Work(int _) {
       _ += 4;
    }

    Pattern matching (模式匹配)

    // C#7.0
    // 模式匹配:匹配一个值是否具有某种特征(例如:是否是某个常量、某个类型、某个变量),
    //          如果是,顺便可将这个值提取到对应特征的新变量中
    // C#7.0中,利用已有的关键字is和switch来扩展,实现模式匹配
    
    // 具有模式匹配的is表达式:不仅能匹配类型,还能匹配表达式
    public static void TestIs(object o) {
        const string IP = "127.0.0.1";
    
        // 匹配常量
        if (o is IP) {
            Console.WriteLine("o is IP");
        }
    
        if (o is null) {
            Console.WriteLine("o is null"); 
        }
    
        // 匹配类型
        if (o is float) {
            Console.WriteLine($"o is float");
        } 
    
        // 匹配类型,并提取值。检测为true,这时候i会被明确赋值
        if (o is int i) {
            Console.WriteLine($"o is int {i}");
        } else {
            return;
        }
    
        // i仍然有效
        // i变量称为模式变量,和out变量一样,统称为表达式变量,作用域都扩展到了外围
        // 表达式变量的范围扩展到了外围,只有在前面的模式匹配为true是才有效
        // 表达式变量为true时,才给变量明确赋值,这样避免了模式不匹配时访问这些变量
        i++;
        Console.WriteLine($"i is {i}");    
    
        if (o is 4 * 4) {
            Console.WriteLine("o is 4*4");
        }
    }
    
    // 可以模式匹配的switch
    // 1. 原来的switch限制为仅仅是string和数字类型的常量匹配,现在解除了
    // 2. switch按照文本顺序匹配,所以需要注意顺序;
    //   (原来switch的分支只匹配一个所以不需要顺序;而现在可以匹配多个,行为变了)
    // 3. case子句后面可以带模式匹配的表达式
    // 4. default最后执行,也就是其他都不匹配时才执行,不管default语句放在什么位置。
    // 5. 如没有default分支,其他也不匹配,则不执行任何switch块代码,直接执行其后面代码
    // 6. case后带var形式变量的匹配,近似于default
    // 7. case 子句引入的模式变量只在switch块内有效
    public static void TestSwitch(object o) {
        switch (o) {
            case "127.0.0.1":
                Console.WriteLine("o is IP");
                break;
            case float f:
                Console.WriteLine($"o is float {f}");
                break;
            case int i when i == 4:
                Console.WriteLine($"o is int {i} == 4");
                break;
            case int i:
                Console.WriteLine($"o is int {i}");
                break;
            case string s when s.Contains("127"):
                Console.WriteLine("o is string, contains 127 ");
                break;
            case string s when s.Contains("abc"):
                Console.WriteLine("o is string, contains 127 ");
                break;
            case var a when a.ToString().Length == 0:
                Console.WriteLine($"{a} : a.ToString().Length == 0");
                break;
            case null:
                Console.WriteLine($"o is null");
                break;
            default:
                Console.WriteLine("default");
                break;
        }
    }

    Ref locals and returns (ref局部变量和返回ref变量)

    // C#7.0
    // C#7.0以前的ref只能用于函数参数,现在可以用于本地引用和作为引用返回
    // 1. 需要添加关键字ref,定义引用时需要,返回引用时也需要
    // 2. 引用声明和初始化必须在一起,不能拆分
    // 3. 引用一旦声明,就不可修改,不可重新再定向
    // 4. 函数无法返回超越其作用域的引用
    
    // 需要添加关键字ref,表示函数返回一个ref int
    public static ref int GetLast(int[] a) {
        if (a == null || a.Length < 1) {
            throw new Exception("");
        }
    
        int number = 18;
    
        // 错误声明: 引用申明和初始化分开是错误的 
        //ref int n1; 
        //n1 = number;
    
        // 正确声明: 申明引用时必须初始化,声明和初始化在一起
        // 添加关键字ref表示n1是一个引用, 
        ref int n1 = ref number;
    
        // n1指向number,不论修改n1或number,对双方都有影响,相当于双方绑定了。
        n1 = 19; 
        Console.WriteLine($"n1:{n1},  number:{number}");
        number = 20;
        Console.WriteLine($"n1:{n1},  number:{number}");
    
        // 语法错误,引用不可被重定向到另一个引用
        //n1 = ref a[2];
    
        // 语法正确,但本质是将a[2]的值赋值给n1引用所指,n1仍指向number
        n1 = a[2];
        Console.WriteLine($"n1:{n1},  number:{number}, a[2]:{a[2]}");
        number = 21;
        Console.WriteLine($"n1:{n1},  number:{number}, a[2]:{a[2]}");
    
    
        // --------------------- 引用返回 ------------------------ 
    
        // 错误:n1引用number,但number生存期限于方法内,故不可返回
        // return ref n1;
    
        // 正确:n2引用a[2],a[2]生存期不仅仅限于方法内,所以可以返回。
        ref int n2 = ref a[a.Length-1];
        return ref n2; // 需要ref返回一个引用
        return ref a[a.Length-1];  // 也可以直接返回一个引用
    }
    
    public static void Main(string[] args) {
        int[] a =  { 0, 1, 2, 3, 4, 5};
    
        // x不是一个引用,函数将值赋值给左侧变量x
        int x = GetLast(a);
        Console.WriteLine($"x:{x}, a[2]:{a[a.Length-1]}");
        x = 99;
        Console.WriteLine($"x:{x}, a[2]:{a[a.Length-1]} 
    ");
    
        // 返回引用,需要使用ref关键字,y是一个引用,指向a[a.Lenght-1]
        ref int y = ref GetLast(a);
        Console.WriteLine($"y:{y}, a[2]:{a[a.Length-1]}");
        y = 100;
        Console.WriteLine($"y:{y}, a[2]:{a[a.Length-1]}");
    
        Console.ReadKey();
    }

    Local functions (本地方法)

    // C#7.0
    // 1. 定义在一个方法体内的函数,称为本地方法
    // 2. 本地方法只在其外部方法体内有效
    // 3. 本地方法可定义在其外部方法的任何地方
    // 4. 外部方法其作用域内参数或变量,都可用于本地方法
    // 5. 本地方法实质被编译为当前类的一个私有成员方法,
    //    但被语言层级限制为只能在其外部方法内使用
    // 6. 由于(5),所以本地方法和类方法一样,没有特殊限制,异步、泛型、Lambda等都可用
    // 7. 常见用例:给迭代器方法和异步方法提供参数检查,因为这两类方法报告错误比较晚
    public static IEnumerable<int> SubsetOfIntArray(int start, int end) {
        if (start < end) {
            throw new ArgumentException($"start({start}) < end({end})");
        }
        return Subset();
    
        IEnumerable<int> Subset() {
            for (var i = start; i < end; i++)
                yield return i;
        }
    }

    More expression-bodied members(更多的 表达式方法体 成员)

    // C#7.0
    // 1. 类成员方法体是一句话的,都可以改成使用表达式,使用Lambda箭头来表达
    // 2. C#6.0中,这种写法只适用于只读属性和方法
    // 3. C#7.0中,这种写法可以用于更多类成员,包括构造函数、析构函数、属性访问器等
    public string Name => "小米喂大象"; //只读属性
    public void SetAge(int age) => Age = age; //方法
    public void Log(string msg) => System.Console.WriteLine($"{Name} : {msg}");
    
    public void Init(string name, string password, int age) {
        Name = name;
        Password = password;
        Age = age;
    }
    public User(string name) => Init(name, "", 18); //构造函数
    public User(string name, string password) => Init(name, password, 18); 
    public ~User() => System.Console.WriteLine("Finalized"); //析构函数(仅示例)
    
    public string password;
    public int Password{ //属性访问器
        get => password;
        set => SetPassword(value);
    }

    Throw expressions (异常表达式)

    // C#7.0
    // 1. c#7.0以前,throw是一个语句,因为是语句,所以在某些场合不能使用。
    //    包括条件表达式、空合并表达式、Lambda表达式等。
    // 2. C#7.0可以使用了, 语法不变,但是可以放置在更多的位置
    public string Name {
        get => name;
        set => name = value ?? 
         throw new ArgumentNullException("Name must not be null");
    }

    Generalized async return types(更泛化的异步返回类型)

    // C#7.0
    // 1. 7.0以前异步方法只能返回void、Task、Task<T>,现在允许定义其他类型来返回
    // 2. 主要使用情景:从异步方法返回Task引用,需要分配对象,某些情况下会导致性能问题。
    //        遇到对性能敏感问题的时候,可以考虑使用ValueTask<T>替换Task<T>。

    Numeric literal syntax improvements(数值字面量语法改进)

    // C#7.0
    // c#7.0为了增加数字的可读性,增加了两个新特性:二进制字面量(ob开头)、数字分隔符(_)
    int b = 123_456_789; // 作为千单位分隔符
    int c = 0b10101010; // 增加了表示二进制的字面量, 以0b开头
    int d = 0b1011_1010; // 二进制字面量里加入数字分割符号_
    float e = 3.1415_926f; // 其他包括float、double、decimal同样可以使用
    double f = 1.345_234_322_333_567_222_777d;
    decimal g = 1.618_033_988_749_894_586_834_365_638_117_720_309_179M;
    long h = 0xff_42_90_b8; // 在十六进制中使用

    C#7.1新特性

    C#7.1是c#的第一个带小数点的版本,意味着快速迭代与发布 
    一般需要在编译器里设置语言版本才能使用

    Async main (异步Main方法)

    // C#7.0中async不能用于main方法,7.1可以
    
    // 以前
    static int Main() {
        return DoAsyncWork().GetAwaiter().GetResult();
    }
    
    // 现在
    static async Task<int> Main() {
        return await DoAsyncWork();
    }
    static async Task Main() { // 没有返回
        await SomeAsyncMethod();
    }

    Default literal expressions (default字面量表达式)

    // 1. C#7.1以前给一个变量设置缺省值,需要使用default(T), C#7.1因为可以推断表达式
    //    的类型,所以可以直接使用default字面量,编译器推断出与default(T)一样的值
    // 2. default字面量可用于以下任意位置:
    //       变量初始值设定项
    //       变量赋值
    //       声明可选参数的默认值
    //       为方法调用参数提供值
    //       返回语句
    //       expression in an expression bodied member
    //      (使用表达式方法体的成员中的表达式)
    
    public class User {
        public string Name { get; set; } = default;
        public int Age { get; set; } = default;
        public int Score => default;
    }
    public static int Test(string name, int age, int score = default) {
        // 以前
        string s1 = default(string);
        var s2 = default(string);
        int i = default(int);
        User u = default(User);
    
        // 现在
        string s3 = default;
        string s4 = "hello";
        s4 = default;
    
        return default;
    }
    
    Test(default, default);

    Inferred tuple element names(tuple元素名可推导)

    // c#7.0引入tuple,7.1增强了tuple中元素的命名,可通过推导来完成tuple中元素的命名
    // 使用变量来初始化tuple时,可以使用变量名给tuple中元素命名
    var name = "xm01";
    var age = 18;
    var p = (name:name, age:18); // 以前,显式命名
    var p2 = (name, age); // 现在,可以推导来命名
    p2.age = 19;

    Reference assembly generation

    编译器添加了两个新的选项用于控制引用程序集的生成:-refout 和 -refonly 
    一个表示只引用,一个表示需要输出引用(故需要指定路径)


    C#7.2

    Reference semantics with value types(只读引用)

    1. 使用值类型变量时,通过可避免堆分配,但需要进行一次复制操作;
    2. 为了取得折中效果, C#7.2提供了一个机制:似值类型不可被修改,但按引用传递。
    // 这几种情况可以:
    // 1. in修饰符修饰的参数,不可被调用的方法修改;
    // 2. ref readonly方式返回一个值,不能被修改;
    // 3. readonly struct声明一个结构,可以作为in参数传递
    // 4. ref struct 声明一个结构,指示直接访问托管内存,始终分配有堆栈。

    Non-trailing named arguments(命名参数不需要在最后)

    1. C#7.2以前,命名参数后面不能再跟位置参数
    2. C#7.2以后,只要命名参数位置正确,可以和位置参数混用
    3. 这样做的目的是: 使用名字参数的调用,一眼可以看出来这个参数的含义, 
      例如参数是一个boolean型的参数,写代码时直接传true,根本看不出什么含义,这时候写上名字可以明确调用接口

    Leading underscores in numeric literals(数字字面量的前导分隔符)

    // 1. C#7.0中提供了下划线来分割数字字面量,以提高可读性,
    //    但是 下划线分割符(_) 不可作为字面量的第一个字符。
    // 2. c#7.2中允许十六进制字面量和二进制字面量以_开头
    
    // 以前
    int x = 0b1011_1010; 
    long y = 0xff_42_90_b8;
    
    // 现在
    int x = 0b_1011_1010; 
    long y = 0x_ff_42_90_b8;

    private protected access modifier (private protected 访问修饰符)

    1. C#7.2添加了一个private protected 访问修饰符
    2. 表示: 
      1)只有自己访问; 
      2)派生类也可以访问,但仅限于在同一个程序集的派生类

    原文链接:https://blog.csdn.net/wsh31415926/article/details/79907545

  • 相关阅读:
    014 接口和抽象类有什么区别?
    013 抽象类能使用 final 修饰吗?
    web前端开发入门_ web前端需要掌握的知识体系
    使用Ajax同步请求时,等待时间过长增加页面提示问题
    h5移动端禁止长按图片保存
    深入浏览器事件循环的本质
    power assert_更智能、优雅的全方位 assert 断言库
    网络串流播放_HTML5如何优化视频文件以便在网络上更快地串流播放
    什么是断点续传?前端如何实现文件的断点续传
    移动端自适应
  • 原文地址:https://www.cnblogs.com/1175429393wljblog/p/9598864.html
Copyright © 2020-2023  润新知