今天我一个朋友, 突然问我, 在程序入口所用的类中, 为什么字段需要加static…
好家伙, 一听就是没懂啥是面向对象! 我也看了网上的一大堆东西, 啊说啥继承, 多态, 反正我是菜鸟的时候是没听懂这些东西, 后来还是我自己摸索出来的 (嘤嘤嘤QAQ)
好的, 我们使用 C# 来做演示, 大概了解一下类的最基本概念. 来整一个, 数组拓展.
演示代码片:
public class ArrayHelper
{
public int[] Source; // 字段
public static int InsCount = 0;
public Arrayhelper() // 构造函数
{
this.Source = new int[]{};
InsCount++;
}
public ArrayHelper(int[] arr) // 有参数的构造函数
{
this.Source = arr;
InsCount++;
}
~ ArrayHelper() // 析构函数
{
InsCount--;
}
public void ResetInsCount()
{
InsCount = 0;
}
public int GetSum() // 方法
{
int sum = 0;
foreach (var i in Source)
sum += i;
return sum;
}
public int GetAverage() // 方法
{
return GetSum() / Source.Length;
}
}
1. 基本概念
1. 啥是对象:
对象, 它的英文是 object, 咱也不懂为啥它被翻译成这种有歧义的词, 没错, 它跟 ‘情人’ 没有任何关系. 意思接近为: “物体”, “事物”, 下面的句子中, ‘对象’ 一词是最接近编程中的 ‘对象’ 的.
- 你要帮助的对象是谁?
- 我们本次整改所针对的对象, 是厂里干部遇事却不作为的事件.
生活中一切东西都可成为对象, 例如一台电脑, 可以说是一个对象, 它有一些成员(字段, 属性, 方法), 例如所装载的硬件. 而对象都有自身的行为(方法), 例如, 汽车, 有点火这个行为.
2. 编程中的类:
编程中的类, 我们可以理解为模板, 或者生产机器, 通过它我们可以创建一个对象(类的实例化). 一个类产生的对象, 就是这个类的实例.
ArrayHelper helper = new ArrayHelper(); // 实例化 ArrayHelper, helper 就是我们示例化产生的对象
3. 构造函数:
构造函数, 就是这个类在构造对象的时候会调用的函数, 例如刚刚 new ArrayHelper() 的时候, 必定会调用类中的这一片代码:
public Arrayhelper() // 构造函数
{
this.Source = new int[]{};
}
我们可以声明多个构造函数, 以使用不同的方式来创建对象.
public ArrayHelper(int[] arr) // 有参数的构造函数
{
this.Source = arr;
}
让我们来使用下这个带参数的构造函数:
int[] myArray = new int[] {1, 2, 3};
ArrayHelper helper = new ArrayHelper(myArray);
4. 对象的字段
在我们的实例代码中, 类中有写一个 int[] 类型的 Source 字段. 我们也说过, 类就好比一个模板. 当一个对象创建后, 它就拥有这个字段
当我们实例化之后, 我们就可以通过实例, 来访问新生成的对象的成员:
ArrayHelper helper = new ArrayHelper();
Console.WriteLine(helper.Source); // 将 helper 的 Source 成员打印出来
这个成员, 是对象所拥有的, 而不是类本身所拥有的, 所以, 下面的代码是错误的:
Console.WriteLine(ArrayHelper.Source); // 会报错, 因为成员是对象的.
每一个对象都有自己独自的成员, 所以, 不同实例的成员, 值是一定相不同的.
5. 对象的方法
在我们的示例代码中, 类中有写一个返回值为 int 的 GetSum 方法, 同字段一样, 每个对象都有自己独自的方法, 所以调用后, 返回值是不一定相同的.
下面是演示:
int[] array1 = new[] {1, 2, 3};
int[] array2 = new[] {1, 2, 3, 4, 5};
ArrayHelper helper1 = new ArrayHelper(array1);
ArrayHelper helper2 = new ArrayHelper(array2);
Console.WriteLine(helper1.GetSum()); // 结果是6
Console.WriteLine(helper2.GetSum()); // 结果是15
同样, 属于对象的方法, 是不能通过类名来调用的, 以下代码是错误的:
Console.WriteLine(ArrayHelper.GetSum()); // 报错, 因为成员是对象的
6. 类的字段:
类也是可以有成员的, 它们独属于某个对象, 它们属于这个类. 这种成员, 被称为静态成员. 例如我们示例代码中的 InsCount 字段.
静态成员是可以被所有实例所访问的, 例如构造函数里面我们指定了为 InsCount 的值增加1, 这就意味着, 我们每实例化一个对象, 这个类的 InsCount 的值都将增加1;
访问类的静态成员, 需要通过类名来访问, 毕竟这个静态成员是属于这个类的. 不可以用实例名来访问, 因为不独属于任何一个实例.
正确示例:
Console.WriteLine(ArrayHelper.InsCount); // 打印类的 InsCount 字段
错误示例:
ArrayHelper helper = new ArrayHelper();
Console.WriteLine(helper.InsCount); // 报错, 因为成员属于类而不是对象
7. 类的方法:
静态方法, 同静态字段一样, 属于整个类, 所有对象可访问, 只能通过类名而无法通过对象名来访问.
例如我们的示例代码中的 ResetInsCount 函数.
正确示例:
ArrayHelper.ResetInsCount();
错误示例:
ArrayHelper helper = new ArrayHelper();
helper.ResetInsCount();
8. 析构函数:
析构函数与构造函数相对应, 析构函数将在对象被释放的时候执行, 例如我们代码片中的析构函数:
~ ArrayHelper()
{
InsCount--;
}
即, 每当一个对象被释放, InsCount 的值都会减少1. 配合构造函数来看, 造成的结果就是, InsCount 字段将始终与当前的实例数量保持一致.
9. this 关键字:
有时候啊, 你可能写函数的参数名, 写了个跟成员一模一样的名字, 结果你想给实例成员赋值的时候, 发现实际上确是给方法的参数赋值了.
在一个类的非静态方法中, 想要访问实例的成员, 你可以用 this 来修饰以避免歧义.
下面是使用了 this 关键字的示例代码
public class TestObj
{
int value;
public void SetValue(int value)
{
this.value = value;
}
public int GetValue()
{
return this.value;
}
}
10. 成员访问限制:
成员有访问限制, 常用的有 公共的(public), 私有的(private), 受保护的(protected), 如果不写修饰符, 那么默认访问限制就是private, 这个我得用一个比较长的代码片来演示了.
修饰符 | 特征 |
---|---|
public | 公有的, 无论谁都可以访问 |
internal | 内部的, 位于同一程序集的可以进行访问 |
protected | 受保护的, 除了这个类的成员, 子类也可以进行访问 |
private | 私有的, 仅有这个类的成员可以访问 |
using System;
namespace Null.Tutorial
{
class TestObj{
{
static int insCount = 0; // 没有写访问限制修饰符, 但是默认是private
public int[] Source;
public TestObj(int[] source)
{
this.Source = source;
insCount++;
}
~TestObj()
{
insCount--;
}
public int GetSum()
{
int sum = 0;
foreach(var i in Source)
sum += i;
return sum;
}
public static int GetInstanceCount()
{
return insCount;
}
}
class Program
{
public static void Main(string[] args)
{
int[] myArr = new int[] {1, 2, 3};
TestObj objIns = new TestObj(myArr);
Console.WriteLine(objIns.insCount); // 报错, 因为在类之外访问私有成员
Console.WriteLine(objIns.GetSum()); // 正常访问, 因为是public
Console.WriteLine(TestObj.GetInstanceCount()); // 正常访问, 因为是public
}
}
}
11. 属性语法糖
C# 还支持属性这种东西. 其实是语法糖. 属性类似于字段, 举个例子, 你就懂了.
class TestObj
{
int field3, field4, field5, field6;
public int Field1 { get; set; }
public int Field2 { get; private set; }
public int Field3 => field3
public int Field4 { get => field4; set => field4 = value; }
public int Field5
{
get
{
return field5;
}
set
{
field5 = value;
}
}
public int Field6 { get => field6; }
}
上面的代码中, Field1 ~ Field5 都是属性.
- Field1 是开放(public)获取值与设置值的属性, 用法跟字段一致.
- Field2 是开放(public)获取值, 但设置值是私有的(private)属性, 即, 在类外面可以直接获取值, 但无法设置.
- Field3 对应私有字段 field3, 它的获取权限是 public, 设置权限是 private
- Field4 对应私有字段 field4, 获取权限是 public, 设置权限是 private
- Field5 是属性的完整写法, 其中包含 get 访问器与 set 访问器, 在没有指定个别访问修饰符时, 它们的权限与整个属性的权限一致.
- Field6 是与私有字段 field6 相对应的, 没有指定 set 访问器的属性, 这意味着它只支持获取值, 不允许设置值, 但是类之内的成员可以直接通过对 field6 进行设置来实现更改属性值.
12. 对象引用
C# 和 Java 中, 有值类型和引用类型, 事实上, 一个 “存储” 对象的字段, 它包含的是这个对象的引用. 这个的话, C++ 程序员肯定熟悉的一批, 可不就是指针嘛.
Object obj = new Object(); // 创建一个对象, 并使用 obj 字段来存储对象引用
Console.WriteLine(obj); // 通过这个对象引用, 可以直接对对象进行操作.
obj = null;
最后一句, 对 obj 字段赋值 null, 肯定会有人以为, 执行完之后, 我们刚刚创建的对象就嗝屁了, 但其实是, 仅仅是这个字段的值为null了.
引用类型的字段就像一个指针, 它有自己的值, 这个值表示对象的内存地址(也有可能是句柄啥的), 总之有这个引用, 就能够找到对象所在的位置. 而给这个字段赋值为null, 也就是改变了这个指针的方向, 让它指向 0x0000, 但事实上对象还存在着.
但是呢, .NET 和 JVM 都有着 GC(Garbage Collection), 即垃圾回收机制. 有一个个步骤来回收这些没用的对象, 到最终, 这些不被引用的托管对象(即被运行时管理的对象)都会被销毁.
还有一个好玩的例子:
int[] myArray = new int[] {1, 2, 3};
int[] another = myArray;
another[0] = 100;
Console.WriteLine(myArray[0]); // 结果是100
其原理就是, myArray 为最开始创建的数组的对象引用, 我们又将这个引用赋值给 another, 那么 another 跟 myArray 的值一样, 指向我们最开始创建的对象. 即, 通过 another来对数组更改, 其实跟通过 myArray 来更改无异. 因为它们表示同一个对象.
13. 非托管对象
注意, 对象的内存是非常大的, 所以如果不好好管理, 很容易造成内存泄漏. 但对于 .NET 和 Java, 这个问题小了一点, 因为大部分类型都可以被 GC 给清理掉.
但是, 还是存在一些非托管对象的, 例如用户自己分配的内存, 以及 MemoryStream 之类的, 这些内存不会被 GC 清理掉, 用户需要自己管理, 一般这种包含非托管内存的类都会继承 IDisposable 接口, 用户可以调用它们的 Dispose 方法来清理掉这个对象的非托管内存.
2. 一些基本认知:
1. 静态与非静态成员访问:
1. 非静态成员可直接访问静态成员:
这个的话, 稍微想想就能理解. 例如一个厂子里刚造出一辆车, 这个车一看就知道是哪个牌子的, 所以可以轻易的找到这个厂子, 也就是说使用这个厂子里的零配件也是非常简单的, 即非静态成员(实例的成员)可直接访问静态成员(类的成员).
2. 静态成员无法直接访问为静态成员:
同样是汽车的例子, 这个车已经离开厂子了, 如果厂子要查看这个车的一些零配件损坏程度, 肯定是不大方便的, 因为你不知道这个车在哪, 即静态成员(类的成员)无法直接访问非静态成员(实例成员).
3. 通过对象引用来访问非静态成员:
静态成员来访问非静态成员, 肯定有办法访问, 例如这辆车在离开厂子前留下个联系方式, 这样厂子就可以找到这辆车. 下面的代码片是一个例子, 必须有对象引用才可以对其操作.
public class TestObj
{
static List<TestObj> instances = new List<TestObj>();
private SelfValue = 0;
public TestObj()
{
instances.Add(this);
}
public int Value => SelfValue;
public static ResetAllInstanceValue()
{
foreach (var ins in instances)
instances.SelfValue = 0;
}
}
喏, 这就是静态方法操作实例成员的示例. 只要你有这个对象的引用, 就可以对这个对象进行操作