Objective-C学习笔记(第一天)
具体的代码,这里就省略了,从上一节的介绍我们可以知道,这种方式就叫做面向过程的方法。由于OC是Steve Jobs在创建NeXT公司时倡导使用的编程工具,后来在NextStep中使用,再后来NextStep更名为Cocoa,由于Cocoa框架中很多源代码来自NextStep库中的Foundation和AppKit,工程师们选择使用NS来作为他们的类前缀,并沿用至今。这表示声明了一个方法,这个方法的
前言:学习视频是https://www.bilibili.com/video/BV1NJ411T78u/
学习笔记是转载自https://beautylife.pub/index.php/12859/
因为本人学习到第三天时才看上面的学习笔记,发现笔记内容和课程顺序是一模一样,本人写的笔记也大差不差,但是我的示例代码会缩减,所以还是转载他人的,以供他人学习
可能会说现在怎么还有人学ios,我也觉得(狗头),但是公司业务有涉及,来看一些快速上手文章也是好的,技多不压身嘛。时间足够的可以结合视频食用效果更佳,时间短也能通过文章有所了解。
1.0 Objective-C 基础
1.1 OC概述
Objective-C简称Obj-C或者OC。
Obiective是面向对象的意思,所以Objective-C就是面向对象的C语言。
所以,OC的本质还是C语言。
OC是在C的基础之上增加了一小部分的面向对象的语法,将C语言复杂的、繁琐的语法封装得更为简单。
因此,OC也是完全兼容C语言的,也就是说在OC语言中可以写任意的C语言代码。
那么OC语言是如何发展的呢?
在20世纪80年代初期,Brad Cox结合了C语言和Smalltalk的优势,设计出了Objective-C语言。
1985年,Steve Jobs创建了NeXT公司,致力于设计经济实惠且功能强大的工作站,并选择Unix作为其工作站的操作系统。后来,NeXT公司设计出了NeXTStep,一套使用OC语言编写的功能强大的界面工具包。
终于在1995年,NeXT公司获得了OC的全部商业版权。
1996年,Apple公司收购了NeXT,NeXTStep得到了Mac开发人员的广泛认可,NeXTStep更名为Cocoa,从此OC成为了开发Mac软件平台上的主力语言。
随着Apple产品的发展,macOS、iOS的崛起以及Mac电脑、iPhone和iPad产品的热销,OC又成为了软件开发工具中极受欢迎的中流砥柱。
2014年,Apple公司在WWDC大会上正式推出Swift,在2015年的WWDC上宣布年底发布的Swift 2.0将会开源。
Swift语言的优点在于快速、现代、安全、互动,且全面优于Objective-C语言。
相信将来Swift一定会取代OC成为macOS、iOS、iPadOS开发的主力语言,但无论如何在学习Swift之前,我们认为OC语言依然还是一门不可多得的优秀软件开发语言。并且学习OC后,进入Swift开发会更加轻松!
1.2 第一个OC程序
创建一个OC的项目,步骤与C语言类似。
第一步,打开Xcode,选择Create a new Xcode project;
第二步,选择macOS,command Line Tool,点击Next;
第三步,给需要建立的程序起名,并在Language中选择Objective-C,点击Next;
第四步,选择保存文件位置,点击Create创建。
这样就创建了我们第一个OC程序。
你可以尝试着在main.m文件中创建如下内容:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
@autoreleasepool
{
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
这样,第一个OC语言的编写就完成了。
(这段代码在Xcode中是默认生成的,在此只为了演示而用。仅供大家观看而已。)
1.3 OC的基础语法
针对上一节我们看到的第一个OC程序的代码,我们来看看OC相对于C多了一些什么:
- 在C的基础上新增了一小部分面向对象的语法
- 将C复杂的、繁琐的的语法封装得更为简单
- OC完全兼容C语言
OC程序的源文件后缀名是.m,m代表message代表OC中最重要的一个机制,消息机制。
而C语言程序的源文件后缀名是.c
main函数仍然是OC程序的入口和出口。
int类型的返回值,代表程序的结束状态。
main函数的参数仍然可以接受用户在运行程序的时候传递数据给程序,同样也可以不写。
#import指令
以#开头,所以是一个预处理指令。
作用:是#include指令的增强版,将文件的内容在预编译的时候拷贝到写指令的地方。
那么#import增强在哪里呢?
同一个文件无论#import多少次,都只会包含一次。而#include则单独做不到这点,必须配合条件编译指令来实现。
能做到这一点的简要原理是:#import指令在包含文件的时候,底层会先判断这个文件是否被包含,如果被包含会略过,否则才会包含。
框架
框架是一个功能集。是指Apple或者第三方事先将一些程序在开发程序的时候经常要用到的功能事先写好。把这些功能封装在一个一个的类或者函数之中。这些函数和类的集合就叫做框架。有点像C语言的函数库。
Foundation框架
Foundation是基础和基本的意思,言下之意在Foundation框架中提供了一些最基础的功能、输入和输出,还有一些数据类型。
所以#import <Foundation/Foundation.h>代表的是引入Foundation框架中的Foundation.h头文件拷贝过来。
Foundation.h这个文件中包含了Foundation框架中的其他所有头文件,所以我们只要包含了这个文件就相当于包含了Foundation框架中所有的头文件,Foundation框架中的所有函数和类就都可以直接使用了。
@autoreleasepool
这是自动释放池。暂时不在这里多加叙述,将来会介绍。
NSLog函数
作用:这个函数相当于C语言中的printf函数的增强版。向控制台输出信息。
语法格式:
NSLog(@“格式控制字符串”,变量列表);
那么相对于printf函数,它增强的地方有哪些?
- NSLog会输出一些调试的相关信息:
2020-07-18 16:18:44.675601+0800 First OC[1504:49178] Hello, World!
- 会显示程序代码执行的时间
- 会显示程序的名称
- 会显示内存的进程编号和线程编号
- 会自动换行
- OC中新增了一些数据类型,这些新增的数据类型只能使用NSLog输出
- NSLog的用法和printf基本相似,一样可以输出变量的值,并且占位符的用法也一样,但是要注意:
- NSLog函数的第一个参数前面必须要加一个@符号
- 如果在字符串的末尾加了一个’\n’,那么NSLog函数的自动换行功能就会失效。
NSString
NSString类型的指针变量,专门用来存储OC字符串的地址。
这里要注意OC的字符串常量,必须要使用1个前缀@符号。
“Jack”,这是一个C语言的字符串
@“Jack”,这是一个OC的字符串常量
NSString类型的指针变量,只能存储OC字符串的地址。
语法格式:
NSString *变量名 = @字符串常量;
注意:如果一个字符串常量前面没有@符号就表明这个字符串常量是一个C字符串,有@符号就是OC字符串常量。在使用NSString定义字符串时,必须要使用带有@前缀的字符串常量。NSString类型的指针变量,只能存储OC字符串。
所以,使用NSLog函数输出OC字符串指针时,要如何操作?
我们知道C语言中输出字符串可以用%s占位符来实现,而NSLog函数中输出OC字符串则需要使用%@占位符来实现。
NS前缀
由于OC是Steve Jobs在创建NeXT公司时倡导使用的编程工具,后来在NextStep中使用,再后来NextStep更名为Cocoa,由于Cocoa框架中很多源代码来自NextStep库中的Foundation和AppKit,工程师们选择使用NS来作为他们的类前缀,并沿用至今。
@符号
作用:
- 将C字符串转换为OC字符串
- OC中的绝大部份的关键字都是以@符号开头
OC中的注释
和C语言一样,OC的注释也分单行注释和多行注释,使用方法以及快捷方式都一样。
函数的定义和调用
OC中的函数定义和调用方法与C语言中函数方法一致!
1.4 OC与C的对比
OC程序的编译、链接和执行
OC程序的编译、链接和执行步骤和C语言基本一致:
以在终端中建立OC语言文件为例
- 在当前工作文件夹下,用touch命令建立一个.m的OC语言程序文件
- 在.m文件中写上符合OC语法规范的源代码
- 使用cc -c命令将源代码编译为目标文件.o文件
- 使用cc命令将.o文件链接源文件
但这里与C语言略有不同,必须要告诉编译器去哪一个框架中找OC文件中使用的函数或者类。
所以命令如下:
cc xx.o -framework 框架名称
程序中用到了哪一个框架中的功能,就把框架名称替换为哪一个框架名字。
例如:
cc first.o -framework Foundation
链接成功后,就会生成一个a.out可执行文件了,执行这个文件即可。
当然,这是在macOS终端中的使用方法,实际使用时我们并不需要这么做,我们在Xcode中只要点击运行按钮,以上所有的事情Xcode都会自动为我们实现的。
OC和C各个阶段的后缀名对比
源文件 | 目标文件 | 可执行文件 | |
C语言 | .c | .o | .out |
OC语言 | .m | .o | .out |
OC和C的数据类型对比
首先,OC中支持C语言中的所有的数据类型。
基本数据类型
int double float char
构造类型
数组 结构体 枚举
指针类型
int* double* float* char*
空类型
void
typedef自定义类型
OC支持以上所有类型
另外,OC还建立了一些新的数据类型:
BOOL类型
可以存储YES或者NO中的任意一个数据(YES和NO都是大写的)
一般情况下BOOL类型的变量用来存储条件表达式的结果。如果条件表达式成立就是YES,条件表达式不成立就是NO。
BOOL类型的本质是typedef signed char BOOL
实际上BOOL类型的变量就是一个有符号的char变量
进一步查看其定义内容会发现:
#define YES ((BOOL)1)
#define NO ((BOOL)0)
所以我们得知,YES实际上就时1,NO实际上就是0。
Boolean类型
可以存储true或者false中的任意一个数据。
一般情况下Boolean类型的变量用来存储条件表达式的结果。如果条件表达式成立就是true,条件表达式不成立就是false。
Boolean类型的本质是typedef unsigned char Boolean;
实际上Boolean类型的变量就是一个无符号的char变量。
进一步查看其定义内容会发现:
#define true 1
#define false 0
所以我们得知,true实际上就时1,false实际上就是0。
class 类
id 万能指针
nil 和C语言的NULL差不多
SEL 方法选择器
block 代码段
后面几种类型后面会逐一介绍。
OC支持C语言中的所有的运算符
+、-、*、/、&&、||等等全部都支持
OC支持C语言中的所有控制语句
if结构、switch-case结构、while循环等等全部都支持。
OC支持C语言中的全部的关键字
OC新增了一些关键字,OC新增的关键字绝大多数都是以@符号开头。例如:
@interface
@implementation
@public
OC函数的定义和调用与C语言完全一致
总结:
OC语言完全兼容C语言,在OC中可以写任意的C代码,并且效果一致。
1.5 面向过程与面向对象
假设我们需要去做一件事情,通常来说可以有两种方式:
第一种,这件事情的每一个步骤都是自己亲自去做,亲力亲为;
第二种,我们去请一位专家,我们只要准备好必要的材料和物资,所有做事的步骤和方式都由专家来帮你做。
在这两种做事的方式中,我们可以看出其中的区别:第一种方式强调的是做事的过程、步骤,自己是主角,自己是彻头彻尾的执行者;而第二种方式强调的是专家,大部分的事情都是专家在做,而自己是一个指挥者,只提供物资不亲自干活。
那么在编程领域,
如果在解决一件事情的时候,每一个步骤都是我们自己亲自去一步步实现,那么解决问题的这种思路就叫做面向过程的解决思路。
如果在解决一件事情的时候,自己并不亲自去做每一件事,而是找一个专家来帮助我们做,这样解决问题的思路,我们称之为面向对象的解决思路。
所以,面向过程和面向对象是解决同一个问题的不同思路。
1.6 代码世界的面向过程与面向对象
假设我们有一组数组需要排序,从我们学习C语言的经验,我们知道我们可以采用冒泡排序或者选择排序的方法来实现。这需要我们写出几段代码,去实现每一个比较、替换和排序,实现这个需求的每一个步骤都是自己写的代码。具体的代码,这里就省略了,从上一节的介绍我们可以知道,这种方式就叫做面向过程的方法。
而面向对象的方法则是采用一个专门做排序的命令来做,不用亲自去写代码来实现每一个步骤。
现在我们来谈谈这两种方法的优缺点。
首先,C语言是一门面向过程的语言,有功能的概念,但是没有去做事的人的概念;
而OC事一门面向对象的语言。
面向过程的方法的缺点:后期的维护和修改很不方便,而面向对象的方法在后期维护和修改十分方便。
1.7 使用面向对象的思维解决问题
当你遇到一个需求的时候,不要亲自去实现。
第一,先看看有没有现成的资源是专门做这件事情的,这些资源通常在框架中,如果有就直接使用;如果没有,就自己造出一个拥有这样的功能的资源,那么造出来的这个资源就可以被重复多次使用。
1.8 类与对象
什么是对象?
对象是现实生活中的一个具体存在,它看得见、摸得着,拿过来就可以直接使用。
什么是类?
类是对一群具有相同特征或者行为的事物的一个统称。它是抽象的,不能被直接使用。如果非要使用类的话,只能去找到这一类食物中的一个具体存在,然后使用这个具体存在。
例如:
食物 是类
水果 是类
苹果 是类
小明早上吃的那个苹果 是对象
交通工具 是类
汽车 是类
奔驰CLS350 是类
楼下停着的那辆奔驰CLS350 是对象
类与对象之间的关系:
类是模板,类的对象是根据这个模板创建出来的。类模板当中有什么,对象中就有什么,不可能多也不可能少。
类的作用:
用来描述一群具有相同特征和行为的事物。
那么如何设计一个类呢?
设计类的三要素:
- 类的名字。你要描述的这类事物叫什么名字。
- 这类事物具有的相同的特征。这类事物拥有什么。
- 这类事物具有的共同的行为。这类事物会做什么。
那么如何找到类呢?
这里要提到名词提炼法。
什么事名词提炼法呢?
分析整个业务流程,分析出现了哪些名词,这些名词就是你要找的类。
例如:
这样一句话:3辆坦克发射了6枚炮弹,击中了3架飞机。
这句话中涉及了三个名词,也就是三个类。
坦克类:
特征:型号、大小、颜色、重量、材质、射程
行为:行驶、发射
炮弹类:
特征:型号、大小、威力、颜色、重量
行为:飞行、爆炸
飞机类:
特征:型号、大小、颜色、重量、材质、行程
行为:飞行
1.9 类的定义
从现实的角度,一定是先有对象然后才有类;但是从代码的角度来说,是先有类然后才有对象。
那么如何去定义一个类呢?
定义类的语法:
- 位置:直接写在源文件中,不要写在main函数中
- 语法:分两部分,一个是类的声明,另一个是类的实现。
类的声明:
@interface 类名 : NSObject
{
这类事物具有的共同的特征,将它们定义为变量
}
功能就是一个方法,将方法的声明写在这里
@end
类的实现:
@implementation 类名
将方法的实现写在这里
@end
例子:
假设定义一个人类。
@interface Person : NSObject
{
NSString *_name;
int _age;
float _height;
}
@end
@implementation Person
@end
(方法的声明和实现后面章节再介绍)
注意:
- 类必须要有声明和实现
- 类名用你描述的事物的名称来命名就可以了,类名的每一个单词的首字母必须要以大写开头
- 用来表示这类事物的共同特征的变量必须要定义在@interface的大括号中
- 定义在大括号之中的用来表示这类事物共同的特征的变量,我们叫做属性、成员变量、实例变量或字段
- 定义的属性的名称必须要以下划线开头,这是编程规范
1.10 类的对象的创建和使用
上一节介绍了类的定义,这里必须要事先声明,光是定义了类,类是无法直接使用的。如果非要使用的话,就必须要先找到这个类的一个具体存在,也就是找到这个类中的一个对象,才能具体使用。
我们知道类和对象的关系是:类中有的东西,这个类的对象也有,不会多也不会少。
那么如何创建一个类的对象呢?
语法:
类名 *对象名 = [类名 new];
例子:Person *p1 = [Person new];
这表示根据Person这个类的模板,创建了一个对象的名字叫做p1。
p1对象的特点:
- 可以直接使用
- 类Person中定义的内容,这个对象p1中也有,不会多也不会少
那么如何使用对象呢?
如何访问对象的属性?
- 默认情况下,对象的属性是不允许被外界直接访问的,如果允许对象的属性可以被外界访问,那么就需要在声明属性的时候加上一个@public关键字。
例如:
@interface Person : NSObject
{
@public
NSString *_name;
int _age;
float _height;
}
@end
- 访问对象的属性的方式:
对象名->属性名 = 值;
对象名->属性名;
例子:
p1->_name = @“Jack”;
p1->_age = 19;
p1->_height = 180.5f;
把这些值取出来:
NSLog(@“p1->_name = %@“,p1->_name);
- 还可以这样访问对象的属性:
(*对象名).属性名;
例子:
(*p1)._name = @“Jack”;
(*p1)._age = 19;
从这里我们可以看出,其实对象的属性访问是类同于C语言中的结构体属性的。
当然,平时我们在使用时一般是使用“对象名->属性名”这种方式。
好,最后通过一个实例来总结一下类和对象的定义与使用。
写一个学生类,属性是姓名、年龄、语文成绩、数学成绩、英语成绩。
@interface Student : NSObject
{
@public
NSString *_name;
int _age;
int _Chinese;
int _Math;
int _English;
}
@end
@implementation Student
@end
Student *s1 = [Studnet new];
s1->_name = @“Jack”;
s1->_age = 19;
s1->_Chinese = 90;
s1->_Math = 80;
s1->_English = 90;
NSLog(@“Name:%@,Age:%d,Chinese:%d,Math:%d,English:%d”,
s1->_name,
s1->_age,
s1->_Chinese,
s1->_Math,
s1->_English
);
1.11 方法的声明、实现和调用
本节介绍类的方法的声明、实现和调用。单独介绍,是因为类的行为(方法)较属性比较复杂。
我们知道,一类事物不仅具有相同的特征(属性)还具有相同的行为(方法)。
行为就是一个功能,C语言中使用函数来表示一个功能,OC的类觉有的行为,我们使用方法来表示。
方法和函数都表示一个功能。
我们把上一节的语法再拷贝过来看一下:
类的声明:
@interface 类名 : NSObject
{
这类事物具有的共同的特征,将它们定义为变量
}
功能就是一个方法,将方法的声明写在这里
@end
类的实现:
@implementation 类名
将方法的实现写在这里
@end
无参数的方法的声明:
- 位置:在@interface的大括号的外面
- 语法:
-(返回值类型)方法名称;
例子:
-(void)run;
表示声明了一个无返回值并且无参数的方法,方法的名字叫run,这个需要写在@interface的带括号外面,与@end的里面
无参数的方法的实现:
- 位置:在@implementation之中实现
- 语法:
-(返回值类型)方法名称
{
方法实现的代码;
}
例子:
@implementation Person
-(void)run
{
NSLog(@“I’m running!”);
}
@end
无参数的方法的调用:
- 方法是无法直接调用的,因为类无法直接使用。必须要先创建对象,有了对象就有类中的属性和方法了,也就可以调用对象的方法了。
- 调用对象的方法的语法:
[对象名 方法名];
例子:
Person *p1 = [Person new];
[p1 run];
带一个参数的方法的声明:
- 位置:在@interface的大括号的外面
- 语法:
-(返回值类型)方法名称:(参数类型)形参名称;
例子:
-(void)eat:(NSString *)foodName;
表示声明了一个无返回值有一个参数的方法,方法的名字叫eat,参数的类型是NSString *类型,参数的名字叫做foodName。这个需要写在@interface的带括号外面,与@end的里面
带一个参数的方法的实现:
- 位置:在@implementation之中实现
- 语法:
-(返回值类型)方法名称:(参数类型)形参名称
{
方法实现的代码;
}
例子:
@implementation Person
-(void)eat:(NSString *)foodname
{
NSLog(@“The %@ is very delicious!”,foodname);
}
@end
带一个参数的方法的调用:
- 方法是无法直接调用的,因为类无法直接使用。必须要先创建对象,有了对象就有类中的属性和方法了,也就可以调用对象的方法了。
- 语法:
[对象名 方法名:实参];
例子:
Person *p1 = [Person new];
[p1 eat:@“Hamburger”];
我们可以发现:方法头中的数据类型都要用一个小括号括起来。
带多个参数的方法的声明:
- 位置:在@interface的大括号的外面
- 语法;
-(返回值类型)方法名称:(参数类型)形参名称1 :(参数类型)形参名称2 :(参数类型)形参名称3;
如果超过3个还可以以此类推的追加,用分号结束。
例子:
-(int)sum:(int)num1 :(int)num2;
这表示声明了一个方法,这个方法的返回值是int类型的,方法的名称叫做sum: :,有两个参数,都是int类型的,分别是num1和num2.
带多个参数的方法的实现:
- 位置:在@implementation之中实现
- 语法:
-(返回值类型)方法名称:(参数类型)形参名称1 :(参数类型)形参名称2 :(参数类型)形参名称3
{
方法实现的代码;
}
例子:
-(int)sum:(int)num1 :(int)num2
{
int num3 = num2 + num1;
return num3;
}
带多个参数的方法的调用:
- 方法是无法直接调用的,因为类无法直接使用。必须要先创建对象,有了对象就有类中的属性和方法了,也就可以调用对象的方法了。
- 语法:
[对象名 方法名:实参1 :实参2 :实参3];
例子:
[p1 sum:10 :20];
注意事项:
- 如果方法只有一个参数,建议最好将这个方法名写成:xxxWith或者xxxWithxxx,增加代码可读性
- 如果方法有多个参数,建议这个方法名称写成:
方法名With:(参数类型)参数名称 and:(参数类型)参数名称 and:(参数类型)参数名称
例如上面的例子可以写成:
sumWith:(int)num1 and:(int)num2
调用时就变成:
[p1 sumWith:10 and:20];
还可以写成:
sumWithNum1:(int)num1 andNum2:(int)num2
调用时就变成:
[p1 sumWithNum1:10 andNum2:20];
代码的可读性增加了很多。
- 同一个类可以创建无数个对象
- 同一个类中的不同对象之间毫无关系,虽然它们拥有相同的属性和相同的方法,但属性的值是不同的
- 在方法的实现当中可以直接访问属性,那么在方法中访问的属性是谁的属性呢?
这个方法是通过哪一个对象来调用的,那么方法中直接访问的属性就是那一个对象的。
接下来是两个例题:
第一题:
设计一个书类
属性:
书名
书号
作者
价格
页数
行为:
显示对象的所有属性的值
代码如下:
#import <Foundation/Foundation.h>
@interface Book : NSObject
{
@public
NSString *_bookName;
NSString *_bookID;
NSString *_Author;
float _price;
int _pages;
}
-(void)show;
@end
@implementation Book
-(void)show
{
NSLog(@"书名《%@》,作者:%@,书号:%@,价格:USD%.2f,页数:%d页",
_bookName,
_Author,
_bookID,
_price,
_pages);
}
@end
int main(int argc, const char * argv[])
{
@autoreleasepool
{
Book *book1 = [Book new];
book1->_bookName = @"简明C语言学习指南";
book1->_bookID = @"ID1523456439";
book1->_Author = @"Zhenjie Zhou";
book1->_price = 2.99f;
book1->_pages = 221;
[book1 show];
}
return 0;
}
第二题:
类名:手机
属性:品牌 颜色 重量 屏幕大小
行为:
查看本机信息
打电话,应该有参数,传入电话号码才可以打电话
发短信,参数为电话号码和短信内容
创建对象并测试
代码如下:
#import <Foundation/Foundation.h>
@interface Phone : NSObject
{
@public
NSString *_brand;
NSString *_color;
float _weight;
NSString *_size;
}
-(void)show;
-(void)call:(int)phoneNumber;
-(void)sendMessage:(int)phoneNumber and:(NSString *)message;
@end
@implementation Phone
-(void)show
{
NSLog(@"本机信息:%@牌 颜色:%@ 重量:%.2fg 尺寸:%@“,
_brand,
_color,
_weight,
_size);
}
-(void)call:(int)phoneNumber
{
NSLog(@"正在拨通%d的电话……",phoneNumber);
}
-(void)sendMessage:(int)phoneNumber and:(NSString *)message
{
NSLog(@"已将短信内容:%@ 发送给%d",message,phoneNumber);
}
@end
int main(int argc, const char * argv[])
{
@autoreleasepool
{
Phone *phone1 = [Phone new];
phone1->_brand = @"Apple";
phone1->_color = @"深空灰";
phone1->_weight = 237.0f;
phone1->_size = @"6.1英寸";
[phone1 show];
[phone1 call:987654321];
[phone1 sendMessage:987654321 and:@"我晚点到!"];
}
return 0;
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)