当前位置:   article > 正文

【Flutter入门到进阶】Dart基础篇---基于对比Java学习Dart_flutter及dart版本选择

flutter及dart版本选择

1 Dart语言特性

1.1 简介

1.1.1 说明

        2011年10月,在丹麦召开的 GOTO 大会上,Google 发布了一种新的编程语言 Dart 。如同 Kotlin 和 Swift 的出现,分别是为了解决 Java 和 Objective-C 在编写应用程序的一些实际问题一样,Dart 的诞生正是要解决 JavaScript 存在的、在语言本质上无法改进的缺陷。

那么,JavaScript 到底有哪些问题和缺陷呢? JavaScript 之父布兰登 · 艾克(Brendan Eich)曾在一次采访中说,JavaScript“几天就设计出来了”。

概括来说,他对JavaScript的设计思路是这样的:

        - 借鉴C语言的基本语法;

        - 借鉴 Java 语言的数据类型和内存管理机制;

        - 借鉴 Scheme 语言,将函数提升到“第一等公民”(first class)的地位;

        - 借鉴 Self 语言,使用基于原型(prototype)的继承机制。

所以,JavaScript 实际上是两类编程语言风格的混合产物:(简化的)函数式编程风格,与(简化的)面向对象编程风格。

由于设计时间太短,一些细节考虑得不够严谨,导致后来很长一段时间,使用 JavaScript 开发的程序混乱不堪。出于对 JavaScript 的不满,Google 的程序员们决定自己写一个新语言来换掉它,所以 Dart 的最初定位也是一种运行在浏览器中的脚本语言。

而为了推广 Dart,Google 甚至将自己的 Chrome 浏览器内置了 Dart VM,可以直接高效地运行 Dart 代码。而对于普通浏览器来说,Google 也提供了一套能够将 Dart 代码编译成JavaScript 代码的转换工具。这样一来,开发者们就可以毫无顾虑地使用 Dart 去开发了,而不必担心兼容问题。再加上出身名门,Dart 在一开始就赢得了部分前端开发者的关注。

但,JavaScript 的生命力似乎比预想的更强大。

原本 JavaScript 只能在浏览器中运行,但 Node.js 的出现让它开始有能力运行在服务端,很快手机应用与桌面应用也成为了 JavaScript 的宿主容器,一些明星项目比如 React、React Native、Vue、Electron、NW(node-webkit)等框架如雨后春笋般崛起,迅速扩展了它的边界。

于是,JavaScript 成为了前后端通吃的全栈语言,前端的开发模式也因此而改变,进入了一个新的世界。就如同 Atwood 定律描述的:凡是能用 JavaScript 写出来的系统,最终都会用 JavaScript 写出来(Any application that can be written in JavaScript, will eventually be written in JavaScript.)。

JavaScript 因为 Node.js 焕发了第二春,而 Dart 就没有那么好的运气了。由于缺少顶级项目的使用,Dart 始终不温不火。2015年,在听取了大量开发者的反馈后,Google 决定将内置的 Dart VM 引擎从 Chrome 移除,这对 Dart 的发展来说是重大挫折,替代 JavaScript 就更无从谈起了。

但,Dart 也借此机会开始转型:在 Google 内部孵化了移动开发框架 Flutter,弯道超车进入了移动开发的领域;而在 Google 未来的操作系统 Fuchsia 中, Dart 更是被指定为官方的开发语言。

与此同时,Dart 的老本行,浏览器前端的发展也并未停滞。著名的前端框架 Angular,除了常见的 TS 版本外,也在持续迭代对应的 Dart 版本[AngularDart](https://github.com/dart-lang/angular)。(不过不得不说的是,这个项目的star一直以来只有可怜的1,100出头)。

也正是因为使用者不多、历史包袱少,所以在经历了这么多的故事后,Dart可以彻底转变思路,成为专注大前端与跨平台生态的语言。

1.1.2 官方文档

https://dart.cn/samples

1.1.3 安装

https://baijiahao.baidu.com/s?id=1683896164474193937&wfr=spider&for=pc

https://blog.csdn.net/weixin_45304503/article/details/127012751

1.2 特性

1.2.2 JIT 与 AOT

        借助于先进的工具链和编译器,Dart 是少数同时支持 JIT(Just In Time,即时编译)和 AOT(Ahead of Time,运行前编译)的语言之一。那,到底什么是 JIT 和 AOT 呢?语言在运行之前通常都需要编译,JIT 和 AOT 则是最常见的两种编译模式。

        - JIT 在运行时即时编译,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但运行速度和执行性能则会因为运行时即时编译受到影响。

        - AOT 即提前编译,可以生成被直接执行的二进制代码,运行速度快、执行性能表现好,但每次执行前都需要提前编译,开发测试效率低。

        总结来讲,在开发期使用 JIT 编译,可以缩短产品的开发周期。Flutter 最受欢迎的功能之一热重载,正是基于此特性。而在发布期使用 AOT,就不需要像 React Native 那样在跨平台 JavaScript 代码和原生 Android、iOS 代码之间建立低效的方法调用映射关系。所以说, Dart 具有运行速度快、执行性能好的特点。

        那么,如何区分一门语言究竟是 AOT 还是 JIT 呢?通常来说,看代码在执行前是否需要编译即可。如果需要编译,通常属于 AOT;如果不需要,则属于 JIT。AOT 的典型代表是 C/C++,它们必须在执行前编译成机器码;而 JIT 的代表,则包括了如 JavaScript、Python 等几乎所有的脚本语言。

1.2.2 内存分配与垃圾回收

        Dart VM 的内存分配策略比较简单,创建对象时只需要在堆上移动指针,内存增长始终是线性的,省去了查找可用内存的过程。

        在 Dart中,并发是通过 Isolate 实现的。Isolate 是类似于线程但不共享内存,独立运行的 worker 。这样的机制,就可以让 Dart 实现无锁的快速分配。

        Dart 的垃圾回收,则是采用了多生代算法。新生代在回收内存时采用“半空间”机制,触发垃圾回收时,Dart 会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存。回收过程中,Dart 只需要操作少量的“活跃”对象,没有引用的大量“死亡”对象则被忽略,这样的回收机制很适合 Flutter 框架中大量 Widget 销毁重建的场景。

1.2.3 单线程模型

        支持并发执行线程的高级语言(比如,C++、Java、Objective-C),大都以抢占式的方式切换线程,即:每个线程都会被分配一个固定的时间片来执行,超过了时间片后线程上下文将被抢占后切换。如果这时正在更新线程间的共享资源,抢占后就可能导致数据不同步的问题。

        解决这一问题的典型方法是,使用锁来保护共享资源,但锁本身又可能会带来性能损耗,甚至出现死锁等更严重的问题。

        这时,Dart 是单线程模型的优势就体现出来了,因为它天然不存在资源竞争和状态同步的问题。这就意味着,一旦某个函数开始执行,就将执行到这个函数结束,而不会被其他 Dart 代码打断。

        所以,Dart 中并没有线程,只有 Isolate (隔离区)。Isolates 之间不会共享内存,就像几个运行在不同进程中的 worker,通过事件循环(Event Looper)在事件队列(Event Queue)上传递消息通信。

1.3 指令

1.3.1 dart 命令支持如下的常用选项:

  1. --enable-asserts - 开启断言
  2. --version - 查看 Dart VM 版本

1.3.2 另外它还支持 Observatory 选项,用于分析和调试 Dart 程序。常用选项包括:

  1. --enable-vm-service - 在本地 8181 端口开启 Observatory 服务
  2. --observe - 相当于同时指定 --enable-vm-service 和 --pause-isolates-on-exit

1.3.3 Dart 语言本身还在快速演化中。所以要注意以下变化:

        1、在 Dart 2.6 以及之前版本,dart2aot 用于生成 AOT 镜像

        2、 Dart 2.6 之后,dart2native 命令取代了 dart2aot (前者是后者的超集)。

        3、dart2native

        dart2native bin/main.dart -o bin/my_app

        4、另一种是 AOT snapshot,它不包含 Dart 运行时,所以需要使用 dartaotruntime 来运行。

        5、dartaotruntime

  1. dart2native bin/main.dart -k aot
  2. dartaotruntime bin/main.aot

1.3.4 但是注意,在后续版本中对应命令也进行了调整

  1. # 编译命令的格式如下: 
  2. # dart compile <subcommand> [arguments]
  3. $ dart compile --help
  4. Compile Dart to various formats.
  5. Usage: dart compile <subcommand> [arguments]
  6. -h, --help    Print this usage information.
  7. Available subcommands:
  8.   aot-snapshot                Compile Dart to an AOT snapshot.
  9.   exe                         Compile Dart to a self-contained executable.
  10.   jit-snapshot                Compile Dart to a JIT snapshot.
  11.   js                          Compile Dart to JavaScript.
  12.   kernel                      Compile Dart to a kernel snapshot.
  13. Run "dart help" to see global options.
  14. # 编译成可执行文件: 
  15. $ dart compile exe hello.dart   # 将hello.dart源文件编译成可执行文件
  16.                                 # 默认输出的文件名与源文件同名, 后缀为.exe
  17.                                 # 上面这条命令生成 hello.exe 文件, 
  18.                                 # 虽然在macOS中生成的文件名是hello.exe, 但是并不影响运行
  19.                                 # "./hello.exe" 运行可执行程序

2 运行区别

2.1 运行方式

2.1.1 java

  1. 运行步骤
  2. javac
  3. java

2.1.2 C

2.1.3 dart

  1. 运行步骤
  2. dart demo.dart

3 变量 

3.1 变量

3.1.1 命名

在Dart语言当中,定义变量的方式是

dataType viriableName = Initial Valute

这里其实和Java是一样的,不仅如此连命名的方式也和Java相同,都是采用驼峰命名的方式。

3.1.2 声明

在声明的时候,除了基本数据类型之外,还有三种变量的声明: var,dynamic,Object

        var:如果没有初始值的时候,var可以变成任意类型,如果有初始值,那么类型会被锁定。

        dynamic:动态任意类型,编译阶段不检查类型。

        Object: 动态任意类型,在编译阶段检查类型。

代码示例

  1. void main(){
  2.   // a 的数据类型可以随意改变,这种类型就是 dynamic
  3.   var a ;
  4.   a = 10;
  5.   a = "dart";
  6.   // 这里我们使用 dynamic 声明
  7.   dynamic d = 10;
  8.   d = 'dart';
  9. }

3.1.3 默认值

        1.没有初始值的变量会自动获取一个默认值null。

        2.一切皆为对象,对象的默认值是null。

3.1.4 私有变量

        与Java不同,Dart没有关键字public、protected和private。如想设置私有变量或函数,则变量和函数名以下划线(_)开头

3.1.5 final和const

        表示不可改变

        const 变量是一个编译时常量,final变量在第一次使用时被初始化

        相同点:

                1、声明的类型可以省略

                2、初始化后不能再赋值

                3、不能和var同时使用

        不同点:

                1、类级别常量,使用static const

                2、const可使用其他const 常量的值来初始化其值

                3、使用const赋值声明,const可以省略

                4、可以更改非final、非const变量的值。即使曾经具有const值

                5、const导致的不可变性是可以传递的

                6、相同的const常量不会再内存中重复创建

                7、const需要是编译时常量

3.2 基本数据类型

3.2.1 Number

3.2.1.1 说明

数值型里面有包含了int型和doubule两种类型,这两种和Java类似,其中int是整数型,double是64-bit双精度浮点数,这两种都是Number类型的子类。

3.2.1.2 代码案例

  1. void main() {
  2.   num a = 10// 整形
  3.   a = 20.1// 浮点型
  4.   print(a);// 20.1
  5.   int i = 10;
  6.   // i = 10.1;     // 这个地方会报错,因为将 int 型的数据改为 double 型 向下转型
  7.   // i = int.parse('10.1'); // 字符串转换为int类型
  8.   i = 10.1.round();// 通过round()方法将double转换为int
  9.   print(i); // 10
  10.   double d = 20.1;
  11.   d = 20// 这个地方不会报错,因为将 double 型的数据改为 int 型,向上转型
  12.   // 向上转型 : (小范围)---》(大范围),这种属于自动转换
  13.   // 向下转型 : (大范围)---》(小范围),这种属于强制转换
  14.   print(d); // 20.0
  15. }

3.2.1.3 数值型的操作

运算符: +、 - 、* 、/ 、 ~/ 、 %

常用属性: isNaN、isEven、isOdd

常用方法:abs()、round()、floor()、ceil()、toInt()、toDouble()

3.2.2 String

Dart字符串是UTF-16编码的字符序列

3.2.2.1 可以使用单引号或者双引号来创建字符

  1. // 这两个是一样的
  2. void main() {  
  3. print("This is a String");
  4. print('This is also a stirng');
  5. }
  6. //单双引号成对嵌套
  7. void main() {
  8.   String str = '单引号中的"双引号"';
  9.   String str1 = "双引号中的'单引号'";
  10.   print(str); // 单引号中的"双引号"
  11.   print(str1); //双引号中的'单引号'
  12.   String str2 = '单引号中的 \'单引号\' ';
  13.   String str3 = "双引号中的 \"双引号\" ";
  14.   print(str2); //单引号中的 '单引号'
  15.   print(str3); //双引号中的 "双引号"
  16. }

3.2.2.2 字符串拼接

  1. void main() {
  2.   String s1 = "First string.....";
  3.   String s2 = "   Second string";
  4.   print(s1 + s2); // First string.....   Second string
  5.   
  6.   // 使用空格即可拼接字符串
  7.   // 使用的是空格拼接,多个空格也可以
  8.   String str1 = '单引号空格字符串' '拼接' '~'// 单引号字符串空格拼接~
  9.   
  10.   // 使用换行符以及空格
  11.   String str2 = '单引号符串'
  12.       '换行了'
  13.       '再加空格'
  14.       '拼接'// 单引号符串换行了再加空格拼接
  15.   // 单双引号 空格拼接
  16.   String str3 = "单双引号空格字符串" '拼接' "~"// 单双引号字符串空格拼接~
  17.   // 单双引号 换行符以及空格
  18.   String str4 = "单双引号符串"
  19.       '换行了'
  20.       '再加空格'
  21.       '拼接'// 单双引号符串换行了再加空格拼接
  22.   
  23.   // 下面这两种情况会报错
  24.   // String blockStr4 = '单引号''''空格';
  25.   // String blockStr5 = "双引号""""空格";
  26. }

3.2.2.3 字符插值

${exprsssion},如果表达式是一个标识符,可以省略{},如果表达式的结果为一个对象,Dart会调用对象的toString()函数来获取一个字符串。

代码

  1. void main() {
  2.   String s1 = "First string.";
  3.   print("String 后面拼接 $s1");// String 后面拼接 First string.
  4.   print("The sum of 1 and 1 equals ${1 + 1}.");// // The sum of 1 and 1 equals 2.
  5. }

3.2.2.4 多行显示

1、使用双引号创建多行字符

  1. var s = 'Firtst'
  2.   'Second'
  3.   "Third";
  4.    print(s);// FirtstSecondThird
  5. 还可以使用(’)和(")

2、使用三引表示多行字符

  1. var multilineString = """This is a
  2.    multiline string
  3.    consistiong of
  4.    multiple lines""";
  5.   print(multilineString);
  6. /*  This is a
  7.    multiline string
  8.    consistiong of
  9.    multiple lines */

3.2.2.5 使用r前缀创建“原始raw”字符串

  1. String s4 ="adbchi\ndfafa";
  2.   print(s4);// 转译 /n
  3.   String s5 =r"adbchi\ndfafa";
  4.   print(s5);// 不转译 /n
  5.   //输出的结果
  6.   adbchi
  7.   dfafa
  8.   adbchi\ndfafa

3.2.3 Bool

Dart的bool和Java类似只有两种类型,一种是true一种是false,但是,不同的是bool对象未初始化的默认值是null。

3.2.4 List

3.2.4.1 实例化数组

  1. // 使用构造创建
  2.   var list = new List();
  3.   //创建一个int类型的list
  4.   List intlist = [123];
  5.   // 创建一个常量List, 不可以改变的List
  6.   List constList = const[10723];

3.2.4.2 Dart就可以直接打印list包含list的元素,java中直接打印list结果是地址值。

  1. var list = [0123456];
  2. print(list); // [0, 1, 2, 3, 4, 5, 6]

3.2.4.3 Dart中List的下标索引和java一样都是从0开始。

3.2.4.4 Dart中List也支持泛型,这点和java一样,同时还可以进行泛型的判断。

  1. var list1 = List<String>();
  2. print(list1 is List<String>); // true 这里的 is 是一种类型判断

3.2.4.5 有增删改查的操作,支持倒叙,自带顺序,洗牌,可以使用+将两个List合并。

var list = [0123456];   print(list);   var list2 = [7891011];   print(list + list2); //[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

3.2.4.6 数组中的常用操作

  1. //  在数组中可以存放多个不同类型的对象
  2.   var list = [123'Flutter'true];
  3.   print(list);      // 输出的结果:[1, 2, 3, Flutter, true]
  4.   // 修改数组下标为2 的值
  5.   list[2] = 'Dart'// 输出的结果:[1, 2, Dart, Flutter, true]
  6.  // 从下标0开始,下标2结束(不包括2),修改之间的值为字符a
  7.   list.fillRange(02'a'); // 输出的结果:[a, a, Dart, Flutter, true]
  8.   // 获取数组的长度
  9.   print(list.length);   // 输出的结果:5
  10.   // 向数组中添加元素
  11.   list.add("value"); // [a, a, Dart, Flutter, true, value]    
  12.  var addList=['8','9'];
  13.   list.addAll(addList);//[a, a, Dart, Flutter, true, value,8,9]
  14.   // 向数组中的指定位置添加元素
  15.   list.insert(1'element');  // [a, element, a, Dart, Flutter, true, value]
  16.  // 向数组中指定位置加入一组元素
  17.  list.insertAll(3, ['1','2','3','4']);// [a, element, a, 1, 2, 3, 4, Dart, Flutter, true, value]
  18.   // 移除数组中的元素
  19.   list.remove("value");//[a, element, a, 1, 2, 3, 4, Dart, Flutter, true]
  20.   // 移除数组中指定的元素
  21.   list.removeAt(1);// [a, a, 1, 2, 3, 4, Dart, Flutter, true]
  22.   // 判断数组中是否有某个元素
  23.   print(list.indexOf("element"));   // 输出结果:-1
  24.   // 可以使用 sort() 函数来排序, 但是由于我们使用定义的数组类型不一样,就不能使用
  25.   var intlist = [1256 ,3];
  26.   // 根据语法提示: List.sort([(int, int) → int compare]) → void
  27.   intlist.sort( (a, b) =>  a.compareTo(b) );// [1, 2, 3, 5, 6]
  28.  //数组翻转
  29.   var newList1=list.reversed.toList();// [true, Flutter, Dart, 4, 3, 2, 1, a, a]
  30.   var bools=list.isEmpty;//判断集合是否为空
  31.   var bools2=list.isNotEmpty;//判断集合是否不为空
  32.  
  33.   var str=list.join('***');//集合转字符串 可以用任意字符拼接
  34.  // a***a***1***2***3***4***Dart***Flutter***true
  35.   var newList2=str.split('***');//字符串转集合
  36.  // [a, a, 1, 2, 3, 4, Dart, Flutter, true]

3.2.4.7 遍历

  1. var list = [123'Flutter'true]; 
  2. //两种遍历方式
  3.   list.forEach((element) {
  4.     print(element);
  5.   });
  6.  
  7.   for (var item in list) {
  8.     print(item);
  9.   }

3.2.5 Set

3.2.5.1 Dart里面的Set和Java类似,也是不能存放重复的元素,是有序的。

3.2.5.2 两种初始化方式

  1. var setName = <dataType>{}
  2. Set<dataType> setName ={}

3.2.5.3 和 JAVA 语言的区别

1、difference:返回set1集合里面有但是set2里面没有的元素集合

  1. Set<String> set1 = {"1","2","3","4","5"};
  2. Set<String> set2 = {"1","2","3"};
  3. print(set1.difference(set2)); // {4, 5}

2、intersection:返回set1和set2的交集

  1. Set set1 = {“1”,“2”,“3”,“4”,“5”};
  2. Set set2 = {“1”,“2”,“3”};
  3. print(set1.intersection(set2)); // {1, 2, 3}

3、union:返回set1和set2的并集

  1. Set<String> set1 = {"1","2","3","4","5"};
  2. Set<String> set2 = {"1","2","3","6"};
  3. print(set1.union(set2)); // {1, 2, 3, 4, 5, 6}

4、 retainAll:set1只保留某些元素(要保留的元素要在原set中存在)

3.2.6 Map

3.2.6.1 创建 Map 集合

  1. void main(){
  2.   // 创建Map 
  3.   var language = {'fisrt''dart''second''java'};
  4.   // 创建不可变的 Map
  5.   var constLanguage = const {'fisrt''dart''second''java'};
  6.   // 通过构造器创建
  7.   var initLanguage = new Map();
  8. }

3.2.6.2 Map 中常用的方法

  1. oid main(){
  2.   // 创建Map 
  3.   var map = {'fisrt''dart''second''java'};
  4.   // 获取长度
  5.   print(map.length);    // 输出结果:2
  6.   // 判断是否为空
  7.   print(map.isEmpty);     // 输出结果:false
  8.   print(map.isNotEmpty);  // 输出结果:true
  9.   // 获取到所有的 key
  10.   print(map.keys);        // 输出结果:(fisrt, second)
  11.   // 获取到所有的 values
  12.   print(map.values);      // 输出结果:(dart, java)
  13.   // 判断是否包含某个key
  14.   print(map.containsKey("key"));    // 输出结果:false
  15.   // 判断是否包含某个value
  16.   print(map.containsValue("key"));    // 输出结果:false
  17.   // 添加一个新的元素进去
  18.   map['third'] = 'key';
  19.   print(map);         // 输出结果:{fisrt: dart, second: java, third: key}
  20.   // 循环打印 代码提示:Map.forEach((String, String) → void f) → void
  21.   map.forEach( (key, value) =>  getMap(key, value) );
  22. }
  23. void getMap(key, value){
  24.   print("key:${key}, value:${value}");
  25. }
  26. void main() {
  27.   var list2 = [12345678910];
  28.   //如果所有的数据都满足条件的就返回true,否则返回false
  29.   var f = list2.every((value) {
  30.     return value > 0;
  31.   });
  32.   print(f); // true
  33.   //如果所有的数据至少有一项满足条件就返回true,否则返回false
  34.   var f2 = list2.any((value) {
  35.     return value > 10;
  36.   });
  37.   print(f2); // false
  38.   //循环遍历,输出满足条件的数据
  39.   var newList5 = list2.where((value) {
  40.     return value > 5;
  41.   });
  42.   print(newList5.toList()); //[6, 7, 8, 9, 10]
  43.   //集合进阶
  44.   List list3 = [
  45.     {
  46.       'province''河北',
  47.       'city': ['石家庄''廊坊''辛集'],
  48.     },
  49.     {
  50.       'province''北京',
  51.       'city': ['朝阳区''海淀区''昌平区'],
  52.     },
  53.     {
  54.       'province''山东',
  55.       'city': ['青岛''淄博''济南'],
  56.     }
  57.   ];
  58.   for (int i = 0; i < list3.length; i++) {
  59.     print(list3[i]);
  60.   }
  61. /*   {province: 河北, city: [石家庄, 廊坊, 辛集]}
  62.   {province: 北京, city: [朝阳区, 海淀区, 昌平区]}
  63.   {province: 山东, city: [青岛, 淄博, 济南]} */
  64.   for (int i = 0; i < list3.length; i++) {
  65.     print(list3[i]['province'] + " ");
  66.     for (int j = 0; j < list3[i]['city'].length; j++) {
  67.       print("   " + list3[i]['city'][j]);
  68.     }
  69.   }
  70.   /* 
  71. 河北 
  72.    石家庄
  73.    廊坊
  74.    辛集
  75. 北京 
  76.    朝阳区
  77.    海淀区
  78.    昌平区
  79. 山东 
  80.    青岛
  81.    淄博
  82.    济南
  83.     */
  84. }

3.2.6.3 Map遍历

  1. map.forEach((key, value) {
  2.     print("map----"+key+' : '+value);
  3.    });
  4.  
  5.  
  6.   //forin循环输出数据
  7.   for(var item in mapList2){
  8.     print("----"+item);
  9.   }

3.2.7 Runes

Runes在Dart当中是字符的UTF-32的编码

  1. Main(){
  2.   Runes runes = new Runes('\u{1f605} \u6211‘);
  3.   var str1 = String.fromCharCodes(runes);  
  4.   print(str1); // 
    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/816307
    推荐阅读
    相关标签