dart

Dart 基础 – 内置类型 – 字符串

内容纲要

版权归作者 ©刘龙宾 所有,本文章未经作者允许,禁止私自转载!

1. 定义字符串

1.1 定义单行字符串

在 Dart 语言中,支持单引号双引号两种方式定义单行字符串,示例代码如下:

void main(List<String> args) {
  String s1 = '使用单引号定义字符串';
  print(s1);

  String s2 = "使用双引号定义字符串";
  print(s2);
}

注意:相同内容的字符串,在内存中会被复用,这样能够节省内存空间。例如:

void main(List<String> args) {
  String s1 = 'liulongbin';
  String s2 = "liulongbin";

  // 输出 true
  // 证明在内存中只创建了一个 'liulongbin' 的字符串,变量 s1 和 s2 都指向这个内存地址
  print(identical(s1, s2));
}

1.2 定义多行字符串

Dart 提供了三个单引号三个双引号两种定义多行字符串的方式。示例代码如下:

void main(List<String> args) {
  // 使用三个单引号定义多行字符串
  String s1 = '''
面朝大海,
春暖花开。''';
  print(s1);

  // 使用三个双引号定义多行字符串
  String s2 = """
好好学习,
天天向上。""";
  print(s2);
}

注意:在定义多行字符串的时候,内部的文本需要顶头对齐,否则会在对应行的前面,出现额外空白字符的情况。

1.3 字符串是不可变的

字符串一旦在内存中被创建,就无法被重新修改,对字符串的操作总是会在内存中创建一个新的字符串。代码示例如下:

void main(List<String> args) {
  // 在内存中开辟第1个字符串 'liulongbin'
  // 并把这个字符串的内存地址赋值给变量 s1 和 s2
  String s1 = 'liulongbin';
  String s2 = 'liulongbin';

  print(s1); // 输出 liulongbin
  print(s2); // 输出 liulongbin

  // 在内存中开辟第2个字符串 '---',
  // 再把内存中的第1个字符串 'liulongbin' 和第2个字符串 '---' 拼接起来,
  // 拼接的结果是:在内存中开辟第3个字符串 'liulongbin---',
  // 最终,把第3个字符串的存储地址,赋值给变量 s2
  s2 += '---';

  print(s1); // 输出 liulongbin,因为 s1 变量指向内存中的第1个字符串
  print(s2); // 输出 liulongbin---,因为 s2 变量指向内存中的第3个字符串
}

2. 拼接与构建字符串

2.1 拼接字符串

Dart 中支持两种拼接字符串的方法,分别是 + 运算符并列放置多个字符串。示例代码如下:

void main(List<String> args) {
  // 使用 + 运算符进行拼接
  String s1 = '中华' + "人民" + '''共和国''' + """万岁""";
  print(s1); // 输出字符串:中华人民共和国万岁

  // 并列放置多个字符串进行拼接
  String s2 = '世界' "人民" '''大团结''' """万岁""";
  print(s2); // 输出字符串:世界人民大团结万岁

  // 即使换行的多个字符串,也能进行拼接
  String s3 = '我'
      "爱"
      '''我的'''
      """祖国""";
  print(s3); // 输出字符串:我爱我的祖国
}

2.2 构建字符串

可以使用 StringBuffer 以代码的方式生成字符串。使用 StringBuffer 的优势是在调用 .toString() 之前,StringBuffer 不会在内存中生成新字符串对象。示例代码如下:

void main(List<String> args) {
  // 1. 创建 StringBuffer 对象
  StringBuffer sb = new StringBuffer();

  // 2. 调用 write() 方法,向 sb 中写入单个字符串片段
  sb.write('从明天起,');
  // 3. 调用 writeln() 方法,向 sb 中写入单个字符串片段,并在末尾写入换行符
  sb.writeln('做一个幸福的人');
  // 4. 调用 writeAll() 方法,一次性向 sb 中写入多个字符串片段,并使用第二个参数 ',' 进行拼接
  // 参数1:要拼接的字符串的数组
  // 参数2:拼接时候的分隔符
  sb.writeAll(['喂马', '劈柴', '周游世界\n'], ',');

  // 5. 调用 StringBuffer 对象的 toString() 方法,生成最终的字符串
  print(sb.toString());
  // 最终在控制台输出的结果为:
  // 从明天起,做一个幸福的人
  // 喂马,劈柴,周游世界
}

3. 转义与原始字符串

在字符串中,可以使用 \ 来表示转义字符,例如 \n 表示换行符,\t 表示制表符,示例代码如下:

void main(List<String> args) {
  String s1 = '面朝大海,\n春暖花开。\t——海子';
  print(s1);
  // 在控制台输出的结果为:
  // 面朝大海,
    // 春暖花开。      ——海子
}

字符串前加上 r 作为前缀创建 “raw” 字符串(即不会被做任何处理(比如转义)的字符串):

void main(List<String> args) {
  String s2 = r'面朝大海,\n春暖花开。\t——海子';
  print(s2);
  // 在控制台输出的结果为(原样输出,\n 不会被当作换行符,\t 也不会被当作制表符):
  // 面朝大海,\n春暖花开。\t——海子
}

4. 字符串插值

在字符串中,可以使用 ${表达式} 的形式往字符串中插入表达式对应的值。如果表达式是一个标识符,还可以省略掉 {}。示例代码如下:

void main(List<String> args) {
  String name = '小明';
  int age = 18;

  // 注意:
  // ${name} 是完整写法,简写形式为 $name
  // ${age} 是完整写法,简写形式为 $age
  // ${age + 2} 不能简写
  String s1 = '大家好我是${name},今年$age岁了,两年后${age + 2}岁。';
  print(s1);
}

5. 字符串的常用方法

5.1 大小写转换

基于字符串的 toUpperCase()toLowerCase() 方法,可以轻松把字符串转为大写小写形式。示例代码如下:

void main(List<String> args) {
  String slogan = 'You guilty,you died';
  // 把字符串转为大写
  String upperSlogan = slogan.toUpperCase();
  print(upperSlogan); // 输出 YOU GUILTY,YOU DIED

  // 把字符串转为小写
  String lowerSlogan = upperSlogan.toLowerCase();
  print(lowerSlogan); // 输出 you guilty,you died
}

5.2 去除字符串两端的空格

调用字符串 trim 相关的方法,可以针对性的去除字符串两端的空格,其中:

  1. trim() 表示去除字符串两端的所有空格
  2. trimLeft() 表示仅去除字符串左侧的所有空格
  3. trimRight() 表示仅去除字符串右侧的所有空格
  4. 注意:以上3个方法,对字符串中间包含的空格没有任何效果。

示例代码如下:

void main(List<String> args) {
  String greeting = '  hello world.  ';
  // 去除字符串两端的空格
  print(greeting.trim());
  // 仅去除字符串左侧的空格
  print(greeting.trimLeft());
  // 仅去除字符串右侧的空格
  print(greeting.trimRight());
}

5.3 判断空字符串

  1. 可以通过字符串的 isEmpty 属性,来判断某一字符串是否为空字符串
    1. 如果 isEmpty 为 true,则证明这个字符串是空字符串
    2. 如果 isEmpty 为 false,则证明这个字符串不是空字符串
  2. 相应的,也可以通过字符串的 isNotEmpty 属性,来判断某一字符串是否是不为空的字符串
    1. 如果 isNotEmpty 为 true,则证明这个字符串是不为空的字符串
    2. 如果 isNotEmpty 为 false,则证明这个字符串不是不为空的字符串

示例代码如下:

void main(List<String> args) {
  String s1 = '';
  print(s1.isEmpty); // 输出 true,表示 s1 是空字符串
  print(s1.isNotEmpty); // 输出 false,表示 s1 不是不为空的字符

  String s2 = '   ';
  print(s2.isEmpty); // 输出 false,表示 s2 不是空字符串(因为空格也是字符)
  print(s2.isNotEmpty); // 输出 true,表示 s2 是不为空的字符串
}

注意:相对于 isEmpty 属性来讲,isNotEmpty 属性的语义比较绕,因此在实际开发中,推荐使用 isEmpty 来判断字符串是否为空。

5.4 在字符串中搜索

可以通过字符串的 contains() 方法,判断某一字符串中是否包含特定的字符串,示例代码如下:

void main(List<String> args) {
  String slogan = 'You guilty,you died';

  // 判断字符串变量 slogan 中是否包含字符串 you
  print(slogan.contains('you')); // 输出 true

  // 判断字符串变量 slogan 中是否包含字符串 your
  print(slogan.contains('your')); // 输出 false
}

可以通过字符串的 startsWith() 方法,判断某一字符串是否以特定的字符串开头。相应的,可以通过字符串的 endsWith() 方法,判断某一字符串是否以特定的字符串结尾。示例代码如下:

void main(List<String> args) {
  String slogan = 'You guilty,you died';

  print(slogan.startsWith('You')); // 输出 true
  print(slogan.startsWith('you')); // 输出 false

  print(slogan.endsWith('died')); // 输出 true
  print(slogan.endsWith('died.')); // 输出 false
}

可以通过字符串的 indexOf() 方法,在字符串内查找特定的字符串第一次出现时对应的索引的位置(注意:索引从 0 开始)。相应的,还可以通过 lastIndexOf() 方法,在字符串内查找特定的字符串最后一次出现时对应的索引的位置。如果查找的结果是 -1,则证明要查找的字符串不包含在当前字符串中。示例代码如下:

void main(List<String> args) {
  String slogan = 'You guilty,you died';

  print(slogan.indexOf('ou')); // 输出 1,证明 ou 第一次出现的位置,是索引为 1 的位置
  print(slogan.lastIndexOf('ou')); // 输出 12,证明 ou 最后一次出现的位置,是索引为 12 的位置

  print(slogan.indexOf('Punisher')); // 输出 -1,证明字符串 Punisher 不包含在字符串中
  print(slogan.lastIndexOf('Punisher')); // 输出 -1,证明字符串 Punisher 不包含在字符串中
}

5.5 分割字符串

字符串的 split() 方法用来分割字符串,分割的结果会得到一个字符串的数组。示例代码如下:

void main(List<String> args) {
  String s1 = 'green red blue cyan';

  // 按照一个空格分割当前的 s1 字符串
  List<String> colors = s1.split(' ');

  // 输出 [green, red, blue, cyan]
  print(colors);
}

在调用 split() 方法时,如果提供了一个空字符串,则会把字符串分割为单个字符的数组。示例代码如下:

void main(List<String> args) {
  String s1 = 'administration';

  // 使用“空字符串”分割 s1 字符串
  List<String> chars = s1.split('');

  // 输出 [a, d, m, i, n, i, s, t, r, a, t, i, o, n]
  print(chars);
}

5.6 基于索引访问单个字符

字符串可以基于 [index] 索引的方式,访问对应的单个字符。示例代码如下:

void main(List<String> args) {
  String s1 = 'administration';

  // 输出索引为3对应的字符串,为 i
  // 注意:索引从 0 开始
  print(s1[3]);
}

5.7 填充字符串

基于字符串的 padLeft(width, padding) 方法,可以在字符串左侧填充指定的字符,从而保证填充后的字符串达到指定的长度。其中:

  1. 第一个参数 width 是一个 int 整数,表示填充完毕后字符串的总长度
  2. 第二个参数 padding 是一个字符串,表示以什么样的字符串进行填充

示例代码如下:

void main(List<String> args) {
  String s1 = '7';

  // 在字符串 7 的左侧以字符串 0 进行填充,填充完毕后总长度为 3。
  // 最后,输出字符串 007
  print(s1.padLeft(3, '0'));
}

相应的,还有一个右侧填充字符串的 padRight(width, padding) 方法,用法与 padLeft() 类似,只是 padRight() 会在右侧进行填充。示例代码如下:

void main(List<String> args) {
  String s1 = '7';

  // 在字符串 7 的右侧以字符串 0 进行填充,填充完毕后总长度为 3。
  // 输出字符串700
  print(s1.padRight(3, '0'));
}

6. 字符和正则表达式

6.1 创建正则实例

正则表达式可以方便的匹配提取字符串中的内容。在 Dart 中可以基于 RegExp 类轻松创建正则对象,语法如下:

RegExp reg = new RegExp(r'正则表达式');

其中 new RegExp() 期间,左侧的 r 表示 raw 原始格式的字符串(原样输出的字符串)。因为正则中往往会包含 \d\s 等这些元字符(具有特殊意义的专用字符),通过指定 r 从而让传递给正则表达式的字符串,能够被当作普通的字面值对待,不需要把它们当作字符串中的转义字符对待。

6.2 判断正则能否成功匹配字符串

正则对象提供了 hasMatch(string) 方法,用来判断当前的正则能否成功匹配指定的字符串。如果能够匹配成功,则 hasMatch() 的返回值为 true,如果 hasMatch() 的返回值为 false 则证明匹配失败。示例代码如下:

void main(List<String> args) {
  // 要匹配的字符串
  String s1 = '2022年11月27日 14:13:30';
  // 创建正则对象,其中 \d 表示匹配一个数字,+ 表示至少出现1次
  RegExp reg = new RegExp(r'\d+');
  // 输出 true,表示匹配成功,字符串变量 s1 中存在数字
  print(reg.hasMatch(s1));

  String s2 = 'abc';
  // 输出 false,表示字符串变量 s2 中不存在数字
  print(reg.hasMatch(s2));
}

6.3 获取匹配成功的第一个结果

正则对象提供了 firstMatch(string) 方法,用来获取匹配成功后的第一个结果,返回值的类型是 RegExpMatch 对象。如果没有任何匹配的结果,则返回 null。示例代码如下:

void main(List<String> args) {
  String s1 = 'abc';
  // 创建正则对象,其中 \d 表示匹配一个数字,+ 表示至少出现1次
  RegExp reg = new RegExp(r'\d+');

  // 调用 firstMatch 对字符串进行正则匹配,
  // 返回值用 match 接收,它的类型是可为 null 的 RegExpMatch 类型
  RegExpMatch? match = reg.firstMatch(s1);
  // 输出 null,证明没有任何匹配的结果
  print(match);
}

如果存在多个匹配结果,则 firstMatch() 只会返回第一个匹配成功的结果,示例代码如下:

void main(List<String> args) {
  // 要匹配的字符串
  String s1 = '2022年11月27日 14:13:30';
  // 创建正则对象,其中 \d 表示匹配一个数字,+ 表示至少出现1次
  RegExp reg = new RegExp(r'\d+');

  // 调用 firstMatch 对字符串进行正则匹配
  RegExpMatch? match = reg.firstMatch(s1);

  // match?[0] 和 match?.group(0) 的作用完全等价,用来获取匹配到的完整内容
  print(match?[0]);       // 输出 2022
  print(match?.group(0)); // 输出 2022
}

6.4 在匹配的结果中提取分组

在创建正则对象期间,可以使用() 来提取分组。如果匹配成功,可以使用 .group(index) 方法或者 [index] 的方式,提取分组内容。注意:

  1. .group(index)[index] 是完全等价的,都可以提取分组内容
  2. .group(0)[0] 永远都是匹配成功的完整内容,从 .group(1)[1] 开始才是提取到的分组内容

示例代码如下:

void main(List<String> args) {
  String s1 = '2022年11月27日 14:13:30';
  // 创建正则对象期间,使用 () 提取分组
  RegExp reg = new RegExp(r'(\d+)年(\d+)月(\d+)日');

  // 获取匹配的第一个结果
  RegExpMatch? match = reg.firstMatch(s1);

  print(match?[0]); // 输出 2022年11月27日
  print(match?[1]); // 提取第1个分组的内容,输出 2022
  print(match?[2]); // 提取第2个分组的内容,输出 11
  print(match?[3]); // 提取第3个分组的内容,输出 27
}

6.5 获取匹配成功的所有结果

正则对象提供了名为 allMatches() 的方法,可以获取到所有的匹配结果,示例代码如下:

void main(List<String> args) {
  // 要匹配的字符串
  String s1 = '2022年11月27日 14:13:30';
  // 创建正则对象,其中 \d 表示匹配一个数字,+ 表示至少出现1次
  RegExp reg = new RegExp(r'\d+');

  // 1. 调用 allMatches() 对字符串进行正则匹配,获取所有的匹配结果
  // 返回值用 matches 来接收,值类型是 Iterable<RegExpMatch>,表示可迭代的 RegExpMatch 集合
  // 2. 为了方便起见,Iterable<RegExpMatch> 可以简写为 var,表示类型推断
  // var matches 定义的变量,会被 Dart 自动推断为 Iterable<RegExpMatch> 类型的变量
  // Iterable<RegExpMatch> matches = reg.allMatches(s1); // 完整写法
  var matches = reg.allMatches(s1); // 基于类型推断的简化写法

  // 3. 使用 for 循环依次输出所有匹配到的内容
  for (var match in matches) {
    print(match[0]);
    // 共循环了 6 次,分别输出:
    // 2022
    // 11
    // 27
    // 14
    // 13
    // 30
  }
}

6.6 使用正则替换字符串

6.6.1 replaceAll

调用字符串的 replaceAll(RegExp, string) 方法,可以把正则匹配到的内容,全部替换为指定的字符串。示例代码如下:

void main(List<String> args) {
  // 需求:把字符串中的“所有数字”替换为“保密”
  String s1 = '姓名:小红,年龄:18,身高:172,体重:99,爱好:高尔夫';
  // 匹配数字的正则
  RegExp reg = new RegExp(r'\d+');

  // 调用字符串的 replaceAll() 方法,
  // 把正则匹配到的内容替换为参数2对应的字符串
  String newS1 = s1.replaceAll(reg, '保密');

  // 输出的结果如下:
  // 姓名:小红,年龄:保密,身高:保密,体重:保密,爱好:高尔夫
  print(newS1);
}

6.6.2 replaceAllMapped

字符串的 replaceAll() 方法只能把匹配到的内容替换为固定的文本,可操作性较低。如果想动态提供要替换的内容,建议使用 replaceAllMapped() 方法。示例代码如下:

void main(List<String> args) {
  // 需求:把字符串中的“数字”,替换相同位数的"x",例如:
  // 数字 9 只有一位数字,所以要替换为 x
  // 数字 110 有三位数字,所以要替换为 xxx
  // 数字 48 有两位数字,所以要替换为 xx
  String s1 = '姓名:小白,年龄:9,身高:110,体重:48,爱好:画画';

  // 匹配数字的正则
  RegExp reg = new RegExp(r'\d+');

  // 调用字符串的 replaceAllMapped() 方法,
  // 把正则匹配到的内容,替换为“参数2”这个函数“动态返回的字符串”
  String newS1 = s1.replaceAllMapped(
      reg, (match) => match[0]!.replaceAll(RegExp(r'.'), 'x'));

  // 输出的结果如下:
  // 姓名:小白,年龄:x,身高:xxx,体重:xx,爱好:画画
  print(newS1);
}

6.7 额外的配置项

在创建正则对象期间,可以指定 caseSensitive, dotAll, multiLine 等额外的配置项。接下来分别对他们进行介绍。

6.7.1 caseSensitive

caseSensitive 选项用来指定正则匹配时是否忽略大小写。如果 caseSensitive 的值是 true,表示严格匹配大小写;如果 caseSensitive 的值是 false,表示忽略大小写。如果不提供 caseSensitive 选项的值,则默认值为 true。示例代码如下:

void main(List<String> args) {
  String s1 = 'Abc';
  // caseSensitive: false 表示正则匹配时忽略大小写
  RegExp reg = new RegExp(r'abc', caseSensitive: false);

  // 正则对象的 hasMatch() 方法,用来判断正则能否成功匹配对应的字符串,
  // true 表示匹配成功;false 表示匹配失败。
  // 这里输出的结果是 true,表示匹配成功
  print(reg.hasMatch(s1));
}

6.7.2 dotAll

dotAll 选项用来指定正则匹配时 . 元字符能否匹配 \r 回车符与 \n 换行符。

dotAll 选项的默认值是 false,表示元字符 . 只能匹配除了 \r\n 之外的任何单个字符。

dotAll 选项设置为 true 之后,元字符 . 可以匹配包括 \r\n 在内的任何单个字符,相当于解除了元字符 . 的封印,使之拥有更强的匹配能力。

示例代码如下:

void main(List<String> args) {
  String s1 = 'Abc\r\n';
  // 1. dotAll 的默认值为 false,所以此时元字符 . 不能匹配 \r 和 \n
  RegExp reg = new RegExp(r'^.+$');
  print(reg.hasMatch(s1)); // 输出 false,表示匹配失败

  // ----

  String s2 = 'Abc\r\n';
  // 2. 把 doAll 设置为 true 之后,元字符 . 能够匹配 \r 和 \n 了
  RegExp reg2 = new RegExp(r'^.+$', dotAll: true);
  print(reg2.hasMatch(s2)); // 输出 true,表示匹配成功
}

6.7.3 multiLine

multiLine 选项用来指定正则能否匹配多行文本。默认值为 false,表示无法匹配多行文本。

multiLine 设置为 true 之后,正则会依次匹配多行文本中的每行文本,只要存在能匹配成功的行,则 hasMatch() 方法会返回 true,表示匹配成功。

示例代码如下:

void main(List<String> args) {
  String s1 = '''
111
2A2
333
456C''';

  // 1. 因为要匹配多行文本,因此要开启 multiLine 选项
  RegExp reg = new RegExp(r'^\d+$', multiLine: true);
  // 2. 输出 true。因为第1行的 '111' 和第3行的 '333' 能够匹配成功
  print(reg.hasMatch(s1));

  // 3. 调用正则的 allMatches() 获取到所有匹配的结果
  for (RegExpMatch match in reg.allMatches(s1)) {
    // 4. 并使用 .group(0) 输出每次匹配到的内容(等同于 match[0])
    print(match.group(0));
    // 5. 共输出了 2 次,分别是:
    // 111
    // 333
  }
}

版权归作者 ©刘龙宾 所有,本文章未经作者允许,禁止私自转载!

一个自由の前端程序员

留言

您的电子邮箱地址不会被公开。 必填项已用*标注