键盘敲烂,月薪过万作业不做,等于没学
当前系列: 编程语言 修改讲义

历史规律

引子:高德纳技术成熟度曲线

问世初期:掌声雷动好评如潮,期望值被拉高,所有语言/架构都蹭“面向对象”的热点

逐步推广:各种问题慢慢浮现,希望越大失望越大,巨大落差,面向对象(OOP)是编程语言发展中的弯路吗?为什么?

归于平静:有好处,但也不是银弹:在最适合的地方使用。到目前为止,面向对象仍然是目前最主流、最有效地处理复杂业务逻辑的手段。(参考:企业级应用


学习目标

语法:非常简单,都应该掌握。

理解为什么:至少要明白“迫不得已”,面向对象是被逼出来,而不是设计出来的!

有意识使用:至少能合理的界定“类的职责”,知道要“避免继承的滥用”,尝试使用多态

方法:观察别人(第三方类库)是如何组织使用的

关于设计模式:两年工作经验以下的新人建议深究。

没写够十万行代码,不要跟我谈设计模式。

因为面向对象(设计模式)解决的是代码的组织(高级)管理问题。

你接触的项目规模有限,对代码复杂性的理解有限,很难体会“面向对象”的作用。


演示用语言

仍然使用C#,因为:

  • Visual Studio,下载安装都很容易(强烈建议:学JavaScript的同学也观摩一下,了解什么是真正的面向对象)
  • C#提供struct和指针的语法,可以更方便的演示值类型和引用类型的区别
  • 这个阶段Java和C#不同的地方很少很少。所有不同处都会专门指出强调
  • JavaScript,唉……,不提也罢,就一蹭“面向对象”热点的,ES6语法类似于Java

另外:

  • 偏重理论和理解,代码演示是为了帮助理解,实战的部分在Java/C#/JavaScript里面细讲
  • 大处着眼,相同处着眼,各语言间细节的、差异性的内容,会在各语言部分详细讲解


类(class)

我们说过,学习完变量赋值分支循环,理论上我们就可以干任何事情了。从函数开始,我们就从代码实现步入了代码管理的领域。

几千上万行的时候,我们可以用几十个函数实现对代码的组织重用,但随着代码量的进一步增加,代码行数以万计数以十万级,函数都数以千计数以万计的时候,我们怎么办呢?光是记忆这些函数,人脑都相当吃力了!(而且还有命名,^_^)

注意:牢记这一点,人脑的局限或者特点,使我们不得不求助于面向对象

@想一想@:我们如何管理成千上万的函数?一个简单的办法,就是分

比如,

  • 和计算相关的函数一类,
  • 和数组相关的函数一类,
  • 和控制台相关的函数一类,
  • ……

这样,我们想调用一个控制台方法的时候,就可以先找到控制台(类),然后在控制台中寻找方法。事实上,我们也确实是这样做的:

Console.WriteLine("源栈欢迎你!");

Console就是一个类,点(.)就相当于“的”,WrtieLine()就是方法名。所以整行代码的意思就是:调用Console类的WrtieLine()方法。

演示:F12转到WrtieLine()

我们也可以自己声明(创建)一个类,这需要使用

class关键字

class Student
{
}

说明:

  • Student是类名,由开发人员定义
  • 花括号({})界定了类的范围

现在回头看看我们一直在使用的Program.cs,它也是一个类文件,其中的class就声明了的Program类,Program中又声明了Main()方法。

Main()是一般都是项目的入口函数。即:这个项目开始运行,就会调用Main()函数。

演示:

  • 额外再建一个Teacher类,里面也有Study()方法;
    public static void Study() //public和static的含义后文细讲
    {
        Console.WriteLine("学习使我快乐!");
    }
    
  • 调用Student.Study();不会走到Teacher类里面去
说明:通常情况下/实际开发中,新建一个类就会创建一个类文件(C#是.cs文件,Java是.java文件),把这个类放在类文件中,即:一个类文件一个类。我们为了演示方便(减少文件切换,暂时把这些类都放一个文件里面)


名称空间(namespace)

但是,当类越来越多,重名的几率就会越来越大,“命名”就会变得越来越困难。

怎么办?一个办法就是加前缀。;但是,“就比如“学生”,这简直太太普遍了。源栈的学生”(YuanZhan.Student),就好多了;如果还担心源栈有重名,就再加一个“大飞哥的源栈的学生”(Dafeige.Yuanzhan.Student)就是了。

实际上,关键字class后面跟的不是类的全名,而是一个部分名 。

部分名之前的前缀就叫做名称/命名空间(Java称之为“包(package)”)。

类的全名是:命名空间+点(.)+部分名,类的名称空间由namespace定义,所以这个类的全名是Yuanzhan.Student。

为什么搞得这么复杂呢?

  • 首先,类名是不能重复的。
  • 其次,名称空间可以按层级组织,比如:
    • Dafeige.Yuanzhan.Student
    • Dafeige.Yuanzhan.Teacher
    • Dafeige.17bang.User
    • Dafeige.17bang.Problem
    • ……
    但层级组织就是为了便于(开发人员)理解和记忆。编译器不关心名称空间的“层级”,换言之,Dafeige和Dafeige.17bang是两个完全不同的名称空间,对编译器而言,两者没有关系。

using和import

然而,每次都使用全名,会非常的麻烦!

所以我们可以在类文件的开头,声明该文件中所使用的类,默认的名称空间究竟是啥:

  • C#:使用using
  • Java:使用import
各语言部分详述。


访问修饰符

你还一定注意到了我们在Study()方法前面加了一个public关键字,这就是:访问修饰符。

它决定了Study()方法能不能在类的外部被调用(访问),包括:

  • public:公开的,可以在外部任何地方使用,(C#中)通常首字母大写
  • private(默认):私有的,只能在自己类的内部使用,通常首字母小写,甚至在前面加上下划线(_)

我们可以在类中再装入一个方法,使用不同的访问修饰符:

        private static void learn(int score)
        {
            score++;
        }

演示:

  • 同一个类中,Study()可以调用learn()
  • 在另外一个类中,无法调用learn()

访问修饰符除了可以修饰方法(以及后文讲到的其他类成员),也可以修饰类本身。但类不能使用private。

访问修饰符对于面向对象至关重要,因为依靠它,才能真正的实现:


封装

把方法分门别类的组织起来,装到不同的类里面,这只做到了装,还没有做到封。只有使用了这些访问修饰符,才能保证让类能够封闭一些逻辑(代码实现),开放一些接口(调用方法)。

为什么需要封闭呢?开放不好么?现在不都提倡“开源”么……

咳咳!这个不是这样理解的。比如说:

  • 办事窗口,内部事务,外部不要干预。
  • 我房间里面的东西,你不要动,你要什么东西,我给你拿

关键的关键,是要能想象:你不是一个人在战斗!有了你我他,就自然有“私有”(孩子长大了,是不是就会有“隐私权”的要求?)

鉴于同学们目前的开发经验,我只能要求大家记牢两点:

  1. 总是尽可能地给最低的访问权限
  2. 不要把访问修饰符当成“安全策略”使用(通过反射,即使private的类成员都可以被获取)


字段

类里面除了可以存放方法,还应该可以存储数据。因为对数据的封装是一个非常普遍的要求。

为什么需要?

比如我们要实现一个报到的功能,于是声明了一个Enroll()的方法,现在这个方法:

  • 需要传入的参数包括:学生的姓名、年龄、身份证号码、之前职业/专业、入栈时间、学习方向、应缴学费、是否需要住宿……
  • 需要返回的数据包括:注册是否成功、未能成功的原因、补救的方式……
怎么办?

声明若干个类,类里面存放相应的数据即可。

public class Student
{
    public static string name;
    public static int age;
    public static string id;
    public static bool isMale;
class EnrollResult
{
    public static bool Failed;
    public static string FailedReason;
最后,将类传入函数即可:
public static EnrollResult Enroll(Student student){}

#体会#:(函数参数和返回值)使用自定义类型(Student和EnrollResult),而不是预定义类型:

public static int Enroll(string name){}


在类中存放数据的成员叫做:字段


实例/对象

PS:前面的代码其实是有问题的……

你注意到没有,我们本来是要讲面向对象的,但我们一直都在讲类啊!对象在哪里?

对象是类的实例,所以,实例化一个类就能得到这个类对象。

我擦!什么鬼?

稍安勿躁,^_^,我们有了类的概念,比如学生类,学生类里可以放一些和学生相关的方法或字段。但有些东西是类的静态成员无法体现的,比如学生的姓名:

Student.Name = "阿泰";

学生这个类的姓名都叫“阿泰”?这说不通。我们只能说某一个学生的姓名叫“阿泰”,另一个学生的姓名叫“波仔”,这样才对。当然,阿泰和波仔都有共同的特点,他们都在飞哥的源栈学习(learn()),都有学习成绩(score),等等,所以他们学生。

没有对象new一个

那如何获得呢?new一个就可以了:

new Student();

这样我们就得到了一个Student类型的对象。

因为对象通过类创建,我们也可以认为:类是创建对象的模板。

在面向对象的世界里,阿泰和波仔都是对象,而且是Student类的对象,因为他们都具有(能调用)Student类所定义的,但是有各自独立的方法(行为)和字段(状态)。

去掉static

演示:对象无法调用静态的字段和方法。

到此为止,无论是字段,还是方法,我们都在其前面加了static,但其实也可以不用static的:

public /*static*/ string name;
public static int age;

public /*static*/ void Learn(int score)
{
    study();
    score++;
}
没有static的字段/方法被称之为实例字段/方法,对应于字段/方法。

对象只能调用Student类中的实例成员(方法和字段):

new Student().name = "阿泰";    //name是实例的
Student.age = 23;               //age是静态的
new Student().Learn(87);        //learn()是实例的

PS:静态成员由类名调用

赋值给变量

@想一想@以下代码运行的结果:

new Student().name = "阿泰";
Console.WriteLine(new Student().name);
结果是不能在控制台输出“阿泰”的。

这是因为new是一个运算符,意味着“构建”,每new一次都会生成一个的对象。

所以上述代码行1和代码2实际上各自生成了两个不相关的对象,新生成的对象上的name字段没有被赋值,所以只有其默认值空。

因此我们通常会在通过new得到一个对象之后,将其赋值给一个同类型的变量:

Student atai = new Student();

#对比体会#:自定义类型的使用:

int age = 10;
let sname = 'atai';


只有通过atai变量指向这个对象,就可以反复的使用了:

atai.name = "阿泰";
Console.WriteLine(atai.name);

注意:我们有时候(日常交流)会把这个变量直接说成对象,有时候(授课)又会区分。大家要根据上下文进行适宜的理解。

另外,不同的对象互不影响(和类的静态成员不同)

字段 vs 变量

注意不要将字段(field)和变量(local variable)混淆:(尤其是当方法调用字段的时候……)

  • 在类中声明的是字段
  • 在方法中声明的是变量:
   class Student
    {
        static int age;                 //字段:类下面直接声明
        static string name = "阿泰";    //可以在声明时赋值

        public static void greet()
        {
            int age = 18;                    //变量:方法中声明
            age++;                           //使用的是变量
            //如果没有age变量的声明,使用字段age
        }
    }

他们还有以下区别(演示)

  1. 字段可以在类中的任何地方声明,在任何地方使用;变量在方法中声明,作用域为自声明开始到声明位置对应的第一个花括号(})结束
  2. 字段前面可以加各种修饰符(比如:访问和static);变量前面不能加修饰符
  3. 变量未赋值不能使用;字段可以使用,值为默认值。默认值依变量类型不同而不同,比如数值类型为0,bool值为false,string类型的默认值是“空字符串”。
  4. 字段可以通过类(或对象)调用;变量只属于当前作用域,外部无法使用
  5. 如果字段和变量同名,在同一个方法中,变量优先

静态 vs 实例

总结一下:


属于
由...调用
选择
静态

类(名)
一定要有充分的理由才使用
实例
对象
对象
优先考虑

PS同一个类中的调用不需要类名或对象名

演示:调用其他类的静态/实例方法

注意他们之间的单向访问问题,即:

在同一类中调用自己类的成员:

  • 实例成员可以访问静态成员,但
  • 静态成员不可以访问实例成员。
学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 编程语言 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码