[干货] 一篇文章完全搞懂面向对象基本概念

今天我一个朋友, 突然问我, 在程序入口所用的类中, 为什么字段需要加static…好家伙, 一听就是没懂啥是面向对象! 我也看了网上的一大堆东西, 啊说啥继承, 多态, 反正我是菜鸟的时候是没听懂这些东西, 后来还是我自己摸索出来的 (嘤嘤嘤QAQ)好的, 我们使用 C# 来做演示, 大概了解一下类的最基本概念. 来整一个, 数组拓展.演示代码片:public class ArrayHelper{ public int[] Source; // 字段 public st.

今天我一个朋友, 突然问我, 在程序入口所用的类中, 为什么字段需要加static…

好家伙, 一听就是没懂啥是面向对象! 我也看了网上的一大堆东西, 啊说啥继承, 多态, 反正我是菜鸟的时候是没听懂这些东西, 后来还是我自己摸索出来的 (嘤嘤嘤QAQ)

好的, 我们使用 C# 来做演示, 大概了解一下类的最基本概念. 来整一个, 数组拓展.

演示代码片:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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, 咱也不懂为啥它被翻译成这种有歧义的词, 没错, 它跟 ‘情人’ 没有任何关系. 意思接近为: “物体”, “事物”, 下面的句子中, ‘对象’ 一词是最接近编程中的 ‘对象’ 的.

  1. 你要帮助的对象是谁?
  2. 我们本次整改所针对的对象, 是厂里干部遇事却不作为的事件.

生活中一切东西都可成为对象, 例如一台电脑, 可以说是一个对象, 它有一些成员(字段, 属性, 方法), 例如所装载的硬件. 而对象都有自身的行为(方法), 例如, 汽车, 有点火这个行为.

2. 编程中的类:

编程中的类, 我们可以理解为模板, 或者生产机器, 通过它我们可以创建一个对象(类的实例化). 一个类产生的对象, 就是这个类的实例.

1
ArrayHelper helper = new ArrayHelper();   // 实例化 ArrayHelper, helper 就是我们示例化产生的对象

3. 构造函数:

构造函数, 就是这个类在构造对象的时候会调用的函数, 例如刚刚 new ArrayHelper() 的时候, 必定会调用类中的这一片代码:

1
2
3
4
public Arrayhelper()           // 构造函数
{
    this.Source = new int[]{};
}

我们可以声明多个构造函数, 以使用不同的方式来创建对象.

1
2
3
4
public ArrayHelper(int[] arr)  // 有参数的构造函数
{
    this.Source = arr;
}

让我们来使用下这个带参数的构造函数:

1
2
int[] myArray = new int[] {1, 2, 3};
ArrayHelper helper = new ArrayHelper(myArray);

4. 对象的字段

在我们的实例代码中, 类中有写一个 int[] 类型的 Source 字段. 我们也说过, 类就好比一个模板. 当一个对象创建后, 它就拥有这个字段

当我们实例化之后, 我们就可以通过实例, 来访问新生成的对象的成员:

1
2
ArrayHelper helper = new ArrayHelper();
Console.WriteLine(helper.Source);         // 将 helper 的 Source 成员打印出来

这个成员, 是对象所拥有的, 而不是类本身所拥有的, 所以, 下面的代码是错误的:

1
Console.WriteLine(ArrayHelper.Source);    // 会报错, 因为成员是对象的.

每一个对象都有自己独自的成员, 所以, 不同实例的成员, 值是一定相不同的.

5. 对象的方法

在我们的示例代码中, 类中有写一个返回值为 int 的 GetSum 方法, 同字段一样, 每个对象都有自己独自的方法, 所以调用后, 返回值是不一定相同的.

下面是演示:

1
2
3
4
5
6
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

同样, 属于对象的方法, 是不能通过类名来调用的, 以下代码是错误的:

1
Console.WriteLine(ArrayHelper.GetSum());    // 报错, 因为成员是对象的

6. 类的字段:

类也是可以有成员的, 它们独属于某个对象, 它们属于这个类. 这种成员, 被称为静态成员. 例如我们示例代码中的 InsCount 字段.

静态成员是可以被所有实例所访问的, 例如构造函数里面我们指定了为 InsCount 的值增加1, 这就意味着, 我们每实例化一个对象, 这个类的 InsCount 的值都将增加1;

访问类的静态成员, 需要通过类名来访问, 毕竟这个静态成员是属于这个类的. 不可以用实例名来访问, 因为不独属于任何一个实例.

正确示例:

1
Console.WriteLine(ArrayHelper.InsCount);     // 打印类的 InsCount 字段

错误示例:

1
2
ArrayHelper helper = new ArrayHelper();
Console.WriteLine(helper.InsCount);        // 报错, 因为成员属于类而不是对象

7. 类的方法:

静态方法, 同静态字段一样, 属于整个类, 所有对象可访问, 只能通过类名而无法通过对象名来访问.

例如我们的示例代码中的 ResetInsCount 函数.

正确示例:

1
ArrayHelper.ResetInsCount();

错误示例:

1
2
ArrayHelper helper = new ArrayHelper();
helper.ResetInsCount();

8. 析构函数:

析构函数与构造函数相对应, 析构函数将在对象被释放的时候执行, 例如我们代码片中的析构函数:

1
2
3
4
~ ArrayHelper()
{
    InsCount--;
}

即, 每当一个对象被释放, InsCount 的值都会减少1. 配合构造函数来看, 造成的结果就是, InsCount 字段将始终与当前的实例数量保持一致.

9. this 关键字:

有时候啊, 你可能写函数的参数名, 写了个跟成员一模一样的名字, 结果你想给实例成员赋值的时候, 发现实际上确是给方法的参数赋值了.

在一个类的非静态方法中, 想要访问实例的成员, 你可以用 this 来修饰以避免歧义.

下面是使用了 this 关键字的示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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私有的, 仅有这个类的成员可以访问
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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# 还支持属性这种东西. 其实是语法糖. 属性类似于字段, 举个例子, 你就懂了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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++ 程序员肯定熟悉的一批, 可不就是指针嘛.

1
2
3
Object obj = new Object();    // 创建一个对象, 并使用 obj 字段来存储对象引用
Console.WriteLine(obj);       // 通过这个对象引用, 可以直接对对象进行操作.
obj = null;

最后一句, 对 obj 字段赋值 null, 肯定会有人以为, 执行完之后, 我们刚刚创建的对象就嗝屁了, 但其实是, 仅仅是这个字段的值为null了.

引用类型的字段就像一个指针, 它有自己的值, 这个值表示对象的内存地址(也有可能是句柄啥的), 总之有这个引用, 就能够找到对象所在的位置. 而给这个字段赋值为null, 也就是改变了这个指针的方向, 让它指向 0x0000, 但事实上对象还存在着.

但是呢, .NET 和 JVM 都有着 GC(Garbage Collection), 即垃圾回收机制. 有一个个步骤来回收这些没用的对象, 到最终, 这些不被引用的托管对象(即被运行时管理的对象)都会被销毁.

还有一个好玩的例子:

1
2
3
4
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. 通过对象引用来访问非静态成员:

静态成员来访问非静态成员, 肯定有办法访问, 例如这辆车在离开厂子前留下个联系方式, 这样厂子就可以找到这辆车. 下面的代码片是一个例子, 必须有对象引用才可以对其操作.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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;
    }
}

喏, 这就是静态方法操作实例成员的示例. 只要你有这个对象的引用, 就可以对这个对象进行操作

Built with Hugo
主题 StackJimmy 设计