首页 > 其他 > 详细

类与对象

时间:2015-09-21 23:58:47      阅读:524      评论:0      收藏:0      [点我收藏+]
1.1 类和对象的关系
 
  为什么要采用面向对象思想进行程序设计与开发
1.可以实现代码的复用
2.符合人思考的方式
  类和对象的定义
1.类的定义:用class关键修饰的叫做类
2.对象的定义:类名定义的数据类型
  类和对象之间关系
1.由对象归纳为类,是归纳对象共性的过程
2.在类的基础上,将状态和行为实体化为对象的过程为实例化
  1.2 定义类
 
  定义类的语法,类主要由成员变量和成员方法构成(暂不提构造函数)
eg:
publicclassStudent
{
//成员变量
intnum = 0;
privatevoidShow()
{
//方法体
}
}
 
 
                                                                                                                                                                                                  1.3创建对象
  对象的创建过程
namespace Temp
{
class Program
{
static void Main(string[] args)
{
Class1 c = new Class1();
}
}
 
class BaseClass
{
int z = 3;
 
public BaseClass()
{
MethodA();
}
 
public virtual void MethodA()
{
Console.WriteLine("BaseClass.MethodA");
}
}
 
class Class1 : BaseClass
{
int x = 1;
int y;
 
public Class1()
{
y = 2;
}
 
public override void MethodA()
{
Console.WriteLine(x + y);
}
}
}
 
以上是一个简单的继承层次结构。不要使用VS测试,脑子分析一下最终输出了什么?
 
分析过程中脑子存在任何疑问的同学,请马上动手测试一下吧,在Main方法中设个断点单步跟踪一下。
 
这里描述一下单步调试的整个过程:
 
黄色光标进入Class1类时跳入了第一句int x = 1;
黄色光标跳过第二句int y 指向 Class1的构造函数;
在执行构造函数的代码块之前跳入了父类,黄色光标指向父类BaseClass 的 int z = 3 语句;
黄色光标指向BaseClass 的构造函数;
黄色光标指向构造函数内的MethodA() 调用;
黄色光标跳向子类Class1 重写的方法MethodA();
查看两个字段发现x=1, y=0;
黄色光标指向Console 语句;
黄色光标从父类构造函数的MethodA() 调用中跳出;
黄色光标从父类构造函数跳出,并再次指向子类构造函数,执行完其中的代码块;
直至执行完毕。
 
 
这里说明了几个顺序问题:
 
对于直接赋值的字段的赋值步骤是在构造函数之前执行,子类字段的赋值是在父类的字段赋值之前;
对于字段的内存分配、初始化等步骤是在我们所能看到的黄色光标进入Class1类的步骤之前;
执行构造函数前会首先执行父类的构造函数;
执行构造函数时 CLR已能识别方法的覆写情况,表明方法的加载过程是在对字段的赋值步骤之前;
int类型的字段在分配内存、初始化阶段已默认赋了0 值(仅限字段中的int,方法中的int变量并非如此)。
总结:当执行new语句时,发生了以下几件事情(更细的情形本文暂不探讨):
 
为字段分配内存并进行初始化;
如果类是第一次加载(即系统中从未创建类的其它对象,或者曾经创建但因不再有引用而被GC 全部回收),则 Copy其实例方法至方法表;
为类中需要直接赋值的字段赋值;
执行构造函数。
  1.4 成员变量和局部变量
 
 
  介绍变量的作用域,说明成员变量和局部变量的定义和区别
1,变量的作用域?
解析:
namespace CSharp
{
public class TestScope
{
public TestScope()
{
 
//sA在TestScope()方法内部有效.
string[] sA = new string[5] { "H", "e", "l", "l", "o" };
 
//块作用域:s6只在foreach循环内部有效。
foreach (string s6 in sA)
{
Console.WriteLine(s6);
}
 
//在这里不能引用s6
//Console.WriteLine(s6);
 
//同样不能重新定义s6
//string s6 = "";
}
}
}
2.成员变量和局部变量的定义和区别?
解析:
成员变量:作为类的成员而存在,直接存在于类中。所有类的成员变 量可以通过this来引用。
局部变量:作为方法或语句块的成员而存在,存在于方法的参数列表和方法定义中。
 
1.成员变量可以被public,protect,private,static等修饰符修饰,而局部变量不能被控制修饰符及static修饰;两者都可以定义成final型。
 
2.成员变量存储在堆,局部变量存储在栈。局 部变量的作用域仅限于定义它的方法,在该方法的外部无法访问它。成员变量的作用域在整个类内部都是可见的,所有成员方法都可以使用它。如果访问权限允许, 还可以在类的外部使用成员变量。
 
3.局部变量的生存周期与方法的执行期相同。 当方法执行到定义局部变量的语句时,局部变量被创建;执行到它所在的作用域的最后一条语句时,局部变量被销毁。类的成员变量,如果是实例成员变量,它和对 象的生存期相同。而静态成员变量的生存期是整个程序运行期。
 
4.成员变量有默认值,基本类型的默认值为0,复合类型的默认值为null。(被final修饰且没有static的必须显式赋值),局部变量不会自动赋值,所以局 部变量在定义后先要赋初值,然后才能使用。
 
5.局部变量可以和成员变量 同名,且在使用时,局部变量具有更高的优先级。
  值类型、引用类型的概念
1.值类型
解析:值类型源于System.ValueType家族,每个值类型的对象dou都有一个独立的内存域用于保护自己的值,值类型数据所在的内存区域成为栈(Stack)
2.引用类型、
解析:引用类型源于System.Object家族
  2 值类型和引用类型
 
  2.1 值类型
 
  值类型的概念
 
  判断类型是否是值类型
Net的类型可以分为值类型和引用类型,值类型通常分配在线程堆栈上,并且不包含任何指向实例数据的指针。引用类型实例分配在托管堆上,变量保存了实例数据的内存引用。
 
1.值类型变量只是进行数据复制,创建一个同值新对象,而引用变量的赋值仅仅是把对象的引用的指针赋给变量,使得变量引用与对象共享同一个内存地址。
 
2.继承结构的区别,引用类型一般都有继承性。但由于值类型是密封的,因此值类型不能作为其它任何类型的基类,但是可以单继承或者是多继承接口。另一个区别是值类型都继承自System。ValueType类,而引用类型则不会继承自System。ValueType类。
 
3.内存分配的区别:值类型通常分配在栈上,它的变量直接包含变量的实例,使用效率相对比较高。而引用类型分配在托管堆上,引用类型的变量通常包含一个指向实例的指针,变量通过指针来引用实例。
 
  2.2 引用类型
 
 
  new运算符
1.用于创建对象和调用构造函数。 例如:
Class1 obj = new Class1();
2.可用于创建匿名类型的实例:
var query = from cust in customers
select new {Name = cust.Name, Address = cust.PrimaryAddress};
3.还用于调用值类型的默认构造函数。 例如:
int i = new int();
 
  对象的内存分配
用类型在内存中的部署
解析:
当创建一个应用类型变量时:
object reference = new object();
 
关键字new将在托管堆上分配内存空间,并返回一个该内存空间的地址。左边的reference位于栈上,是一个引用,存储着一个内存地址;而这个地址指向的内存(位于托管堆)里存储着其内容(一个System.Object的实例)。下面为了方便,简称引用类型部署在托管推上。
考虑数组:
int[] reference = new int[100];
 
根据定义,数组都是引用类型,所以int数组当然是引用类型(即reference.GetType().IsValueType为false)。
而int数组的元素都是int,根据定义,int是值类型(即reference[i].GetType().IsValueType为true)。那么引用类型数组中的值类型元素究竟位于栈还是堆?
如果用WinDbg去看reference[i]在内存中的具体位置,就会发现它们并不在栈上,而是在托管堆上。
实际上,对于数组:
TestType[] testTypes = new TestType[100];
 
如果TestType是值类型,则会一次在托管堆上为100个值类型的元素分配存储空间,并自动初始化这100个元素,将这100个元素存储到这块内存里。
如果TestType是引用类型,则会先在托管堆为testTypes分配一次空间,并且这时不会自动初始化任何元素(即testTypes[i]均为null)。等到以后有代码初始化某个元素的时候,这个引用类型元素的存储空间才会被分配在托管堆上。
  2.3 比较值类型和引用类型
1.值类型
C#的所有值类型均隐式派生自System.ValueType:
结构体:struct(直接派生于System.ValueType);
数值类型:
整型:sbyte(System.SByte的别名),short(System.Int16),int(System.Int32),long(System.Int64),byte(System.Byte),ushort(System.UInt16),uint(System.UInt32),ulong(System.UInt64),char(System.Char);
浮点型:float(System.Single),double(System.Double);
用于财务计算的高精度decimal型:decimal(System.Decimal)。
bool型:bool(System.Boolean的别名);
用户定义的结构体(派生于System.ValueType)。
枚举:enum(派生于System.Enum);
可空类型(派生于System.Nullable<T>泛型结构体,T?实际上是System.Nullable<T>的别名)。
每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值。例如:
int i = new int();
 
等价于:
Int32 i = new Int32();
 
等价于:
int i = 0;
 
等价于:
Int32 i = 0;
 
使用new运算符时,将调用特定类型的默认构造函数并对变量赋以默认值。在上例中,默认构造函数将值0赋给了i。MSDN上有完整的默认值表。
关于int和Int32的细节,在我的另一篇文章中有详细解释:《理解C#中的System.Int32和int》。
所有的值类型都是密封(seal)的,所以无法派生出新的值类型。
值得注意的是,System.ValueType直接派生于System.Object。即System.ValueType本身是一个类类型,而不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。
可以用Type.IsValueType属性来判断一个类型是否为值类型:
TestType testType = new TestType ();
if (testTypetype.GetType().IsValueType)
{
Console.WriteLine("{0} is value type.", testType.ToString());
}
2.引用类型
C#有以下一些引用类型:
数组(派生于System.Array)
用户用定义的以下类型:
类:class(派生于System.Object);
接口:interface(接口不是一个“东西”,所以不存在派生于何处的问题。Anders在《C# Programming Language》中说,接口只是表示一种约定[contract]);
委托:delegate(派生于System.Delegate)。
object(System.Object的别名);
字符串:string(System.String的别名)。
可以看出:
引用类型与值类型相同的是,结构体也可以实现接口;
引用类型可以派生出新的类型,而值类型不能;
引用类型可以包含null值,值类型不能(可空类型功能允许将null 赋给值类型);
引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将复制包含的值。
对于最后一条,经常混淆的是string。我曾经在一本书的一个早期版本上看到String变量比string变量效率高;我还经常听说String是引用类型,string是值类型,等等。例如:
string s1 = "Hello, ";
string s2 = "world!";
string s3 = s1 + s2;//s3 is "Hello, world!"
 
这确实看起来像一个值类型的赋值。再如:
string s1 = "a";
string s2 = s1;
s1 = "b";//s2 is still "a"
 
改变s1的值对s2没有影响。这更使string看起来像值类型。实际上,这是运算符重载的结果,当s1被改变时,.NET在托管堆上为s1重新分配了内存。这样的目的,是为了将做为引用类型的string实现为通常语义下的字符串。
 
   2.4对象的生命周期
无论是指类型的变量或是类类型的变量,其存储单元都是在栈中分配的,唯一不同的是类类型的变量实际上存储的是该类对象的指针,相当于vc6中的CType*,只是在.net平台的语言中将指针的概念屏蔽掉了。我们都知道栈的一大特点就是LIFO(后进先出),这恰好与作用域的特点相对应(在作用域的嵌套层次中,越深层次的作用域,其变量的优先级越高)。因此,再出了“}”后,无论是值类型还是类类型的变量(对象指针)都会被立即释放(值得注意的是:该指针所指向的托管堆中的对象并未被释放,正等待GC的回收)。.NET中的栈空间是不归GC管理的,GC仅管理托管堆。
我想就我的理解简要说明一下:
1、GC只收集托管堆中的对象。
2、所有值类型的变量都在栈中分配,超出作用域后立即释放栈空间,这一点与VC6完全
一样。
3、区别类类型的变量和类的对象,这是两个不同的概念。类类型的变量实际上是该类对
象的指针变量。如C#中的定义CType myType;与VC6中的定义CType* myType;是完全一
样的,只是.net语言将*号隐藏了。与VC6相同,必须用new 关键字来构造一个对象,
如(C#):CType myType=new CType();其实这一条语句有两次内存分配,一次是为类类
型变量myType在栈中分配空间(指针类型所占的空间,对32位系统分配32位,64位
系统则分配64位,在同一个系统中,所有指针类型所占的内存空间都是一样的,而
不管该类型的指针所指向的是何种类型的对象),另一次是在托管堆(GC所管理的
堆)中构造一个CType类型的对象并将该对象的起始地址赋给变量myType。正因为如
此才造成了在同一个作用域中声明的类类型的变量和该类型的对象的生存期不一样。
 
  System.GC类型
GC就是垃圾回收器,一般来说系统会自动检测不会使用的对象或变量进行内存的释放,不需要手动调用,用Collect()就是强制进行垃圾回收,使内存得到及时的释放,让程序效率更高.
给个例子:使用Optimized 设置对第 2代对象进行垃圾回收。
using System;
class Program
{
static void Main(string[] args)
{
GC.Collect(2, GCCollectionMode.Optimized);
}
}
 
 
详细解释 https://msdn.microsoft.com/zh-cn/library/system.gc.aspx
  3 成员方法
 
  3.1 方法的定义和调用
 
 
 
 
 
  方法签名
c# 中方法签名 指的是?
方法签名由方法名称和一个参数列表(方法的参数顺序和类型)组成。
 
注意:方法的签名并不包括方法的返回值。虽然每个重载方法可以有不同的返回类型,单返回类型并不足以区分所条用的是哪个方法。
 
在C#中,同一个类中的两个或两个以上的方法可以有不同的名字,只要他们的参数声明不同即可。在这种情况下,该方法就被称为重载(overload),这个过程称为方法重载(method overloading)。方法重载是C#最有用的特性之一。
 
当一个方法被调用时,C#用方法签名确定调用哪一个方法。因此,每个重载方法的参数列表必须是不同的。虽然每个重载方法可以有不同的返回类型,单返回类型并不足以区分所条用的是哪个方法。当C#调用一个重载方法时,参数与条用参数相匹配的方法被执行。
重写(override)是指,派生类对基类的方法的实现进一步改进。
不能重写非虚方法或静态方法。重写的基方法必须是virtual、abstract 或 override的。为什么 override也可以重写呢?因为基类中的override实际上是对基类的基类进行的重写,由于继承可传递,所以也可以对基类中override的方法进行重写。
override声明不能更改 virtual方法的可访问性。override方法和 virtual方法必须具有相同的访问级别修饰符。
不能使用修饰符new、static、virtual 或 abstract来修改 override 方法。
重写属性声明必须指定与继承属性完全相同的访问修饰符、类型和名称,并且被重写的属性必须是virtual、abstract 或 override 的。
  3.2 方法的参数传递
 
 
 
 
  参数类型ref、out和params的区别和注意事项
示例代码:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace RefDifferenceToOut
7 {
8 class Program
9 {
10 static void Main(string[] args)
11 {
12 //这里做为ref类型的参数必须初始化。ref类型参数传入未初始化变量会报错。
13 string refTestStr = string.Empty;
14
15 //作为out类型参数可以不初始化,因为out类型参数是在函数内部赋值。
16 string outTestStr = string.Empty;
17
18 Program p = new Program();
19
20 Console.WriteLine("默认空值输出:");
21 Console.WriteLine(p.UseRef(ref refTestStr));
22 Console.WriteLine(p.UseOut(out outTestStr));
23 Console.WriteLine("refTestStr:" + refTestStr);
24 Console.WriteLine("outTestStr:" + outTestStr);
25 Console.WriteLine("-------------------");
26
27 refTestStr = "1";
28 outTestStr = "2";
29
30 Console.WriteLine("从新赋值:refTestStr = /"1/";outTestStr = /"2/";");
31 Console.WriteLine(p.UseRef(ref refTestStr));
32 Console.WriteLine(p.UseOut(out outTestStr));
33 Console.WriteLine("refTestStr:" + refTestStr);
34 Console.WriteLine("outTestStr:" + outTestStr);
35 Console.WriteLine("-------------------");
36
37
38 refTestStr = "3";
39 outTestStr = "4";
40 Console.WriteLine("从新赋值:refTestStr = /"3/";outTestStr = /"4/";");
41 Console.WriteLine(p.UseRef(ref refTestStr));
42 Console.WriteLine(p.UseOut(out outTestStr));
43 Console.WriteLine("refTestStr:" + refTestStr);
44 Console.WriteLine("outTestStr:" + outTestStr);
45 Console.WriteLine("-------------------");
46
47
48 Console.WriteLine("--------params-------");
49 p.param("str_a", "2", "3", "4");
50
51
52 }
53
54 public string UseRef(ref string refTest)
55 {
56 return refTest += "rrrrrref";
57 }
58
59 public string UseOut(out string outTestStr)
60 {
61 //在这里需要给outTest从新赋值
62 outTestStr = "0";
63 outTestStr += "oooooout";
64 return outTestStr;
65 }
66
67 ///< summary>
68 /// params参数练习。
69 ///< /summary>
70 ///< param name="a">同是string参数</param>
71 ///< param name="list">string类型列表参数</param>
72 public void param(string a,params string[] list)
73 {
74 Console.WriteLine(list.ToString());
75
76 int i = 0;
77
78 Console.WriteLine(a);
79 foreach (string s in list)
80 {
81 Console.WriteLine(i++ +"_"+ s);
82 }
83 }
84 }
85 }
86
 
输出结果:
默认空值输出:
rrrrrref
0oooooout
refTestStr:rrrrrref
outTestStr:0oooooout
-------------------
从新赋值:refTestStr = "1";outTestStr = "2";
1rrrrrref
0oooooout
refTestStr:1rrrrrref
outTestStr:0oooooout
-------------------
从新赋值:refTestStr = "3";outTestStr = "4";
3rrrrrref
0oooooout
refTestStr:3rrrrrref
outTestStr:0oooooout
-------------------
--------params-------
System.String[]
str_a
0_2
1_3
2_4
 
 
总结:
ref和out都对函数参数采用引用传递形式——不管是值类型参数还是引用类型参数,并且定义函数和调用函数时都必须显示生命该参数为ref/out形式。两者都可以使函数传回多个结果。
 
两者区别:
 
两种参数类型的设计思想不同,ref的目的在于将值类型参数当作引用型参数传递到函数,是函数的输入参数,并且在函数内部的任何改变也都将影响函数外部该参数的值;而out的目的在于获取函数的返回值,是输出参数,由函数内部计算得到的值再回传到函数外部,因此必须在函数内部对该参数赋值,这将冲掉函数外部的任何赋值,使得函数外部赋值毫无意义。
 
表现为:
 
1、out必须在函数体内初始化,这使得在外面初始化变得没意义。也就是说,out型的参数在函数体内不能得到外面传进来的初始值。
2、ref必须在函数体外初始化。
3、两者在函数体内的任何修改都将影响到函数体外面。
 
 
 
  判断方法重载的依据
同一个类中定义多个方法名称相同、参数列表(参数个数、参数类型)不同的方法,称为方法重载
  4 构造函数
 
 
 
  构造函数的特点
1.方法名与类名相同
2.没有返回值类型
3.主要完成对象的初始化工作
  构造函数的分类
1.无参构造
2.带参构造
3.隐式构造
  构造函数及其作用
实例化对象,初始化数据的
  3.2 重载构造函数
 
 
 
 
  从其他构造函数调用构造函数
其实就是使用this来实现的。看一下例子就会明白的了。
class Class1
{
public Class1()
{
//Code 1
}
 
public Class1(string s):this()
{
//Code 2
}
 
public Class1(int i, string j) : this(j)
{
//Code 3
}
}
 
  this关键字的用法
this,只能在类的定义代码中使用,代表的是自己,凡是用this的地方,其实都是可以省略的。
 
而像下面这种this的使用,是用作扩展方法的第一个参数的修饰符,这样不会和参数的name重复不清
public Employee(string name, string alias)
{
// Use this to qualify the fields, name and alias:
this.name = name;
this.alias = alias;
}
  3.3 构造函数注意事项
方法名与类名相同 没有返回值
 
 
  4.2 ToString()方法
任何对象都会自动调用ToString()
这是编译器的一个机制
 
 
  UML图的一种,静态建模
http://www.cnblogs.com/ywqu/category/223486.html
  类图组成
http://developer.51cto.com/art/201007/210830.htm
 
在UML建模中UML类图的java代码表现
 
UML类图元素
 
1.类(Classes)
 
类包含3个组成部分。第一个是Java中定义的类名。第二个是属性(attributes)。第三个是该类提供的方法。
属性和操作之前可附加一个可见性修饰符。加号(+)表示具有公共可见性。减号(-)表示私有可见性。#号表示受保护的可见性。省略这些修饰符表示具有package(包)级别的可见性。如果属性或操作具有下划线,表明它是静态的。在操作中,可同时列出它接受的参数,以及返回类型,如下图所示:
 
2.包(Package)
 
UML类图中包是一种常规用途的组合机制。UML中的一个包直接对应于Java中的一个包。在Java中,一个包可能含有其他包、类或者同时含有这两者。进行建模时,你通常拥有逻辑性的包,它主要用于对你的模型进行组织。你还会拥有物理性的包,它直接转换成系统中的Java包。每个包的名称对这个包进行了惟一性的标识。
 
3.接口(Interface)
 
接口是一系列操作的集合,它指定了一个类所提供的服务。它直接对应于Java中的一个接口类型。接口既可用下面的那个图标来表示(上面一个圆圈符号,圆圈符号下面是接口名,中间是直线,直线下面是方法名),也可由附加了<<interface>>的一个标准类来表示。通常,根据接口在类图上的样子,就能知道与其他类的关系。
 
UML类图关系:
 
1.依赖(Dependency)
 
实体之间一个“使用”关系暗示一个实体的规范发生变化后,可能影响依赖于它的其他实例。更具体地说,它可转换为对不在实例作用域内的一个类或对象的任何类型的引用。其中包括一个局部变量,对通过方法调用而获得的一个对象的引用(如下例所示),或者对一个类的静态方法的引用(同时不存在那个类的一个实例)。也可利用“依赖”来表示包和包之间的关系。由于包中含有类,所以你可根据那些包中的各个类之间的关系,表示出包和包的关系。
 
2.关联(Association)
 
实体之间的一个结构化关系表明对象是相互连接的。箭头是可选的,它用于指定导航能力。如果没有箭头,暗示是一种双向的导航能力。在Java中,关联转换为一个实例作用域的变量,就像图E的“Java”区域所展示的代码那样。可为一个关联附加其他修饰符。多重性(Multiplicity)修饰符暗示着实例之间的关系。在示范代码中,Employee可以有0个或更多的TimeCard对象。但是,每个TimeCard只从属于单独一个Employee。
 
3.聚合(Aggregation)
 
UML类图中聚合是关联的一种形式,代表两个类之间的整体/局部关系。聚合暗示着整体在概念上处于比局部更高的一个级别,而关联暗示两个类在概念上位于相同的级别。聚合也转换成Java中的一个实例作用域变量。
关联和聚合的区别纯粹是概念上的,而且严格反映在语义上。聚合还暗示着实例图中不存在回路。换言之,只能是一种单向关系。
 
4.合成(Composition)
 
合成是聚合的一种特殊形式,暗示“局部”在“整体”内部的生存期职责。合成也是非共享的。所以,虽然局部不一定要随整体的销毁而被销毁,但整体要么负责保持局部的存活状态,要么负责将其销毁。
局部不可与其他整体共享。但是,整体可将所有权转交给另一个对象,后者随即将承担生存期职责。Employee和TimeCard的关系或许更适合表示成“合成”,而不是表示成“关联”。
 
5.泛化(Generalization)
 
UML类图中泛化表示一个更泛化的元素和一个更具体的元素之间的关系。泛化是用于对继承进行建模的UML元素。在Java中,用extends关键字来直接表示这种关系。
 
6.实现(Realization)
 
实例关系指定两个实体之间的一个合同。换言之,一个实体定义一个合同,而另一个实体保证履行该合同。对Java应用程序进行建模时,实现关系可直接用implements关键字来表示。
 
 
 

类与对象

原文:http://www.cnblogs.com/1600kun/p/4827673.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!