引言:开源贡献作为编程教育的核心价值

在现代软件开发教育中,开源社区贡献已经成为连接学术学习与工业实践的关键桥梁。作为编程导师,引领学生参与开源项目不仅能帮助他们掌握真实世界的编程技能,更能培养协作精神、代码审查意识和问题解决能力。开源贡献的核心价值在于它提供了一个真实的、多元化的学习环境,学生在这里面对的不是预设的练习题,而是来自全球开发者社区的实际需求和挑战。

开源社区贡献的教育意义远超传统课堂作业。首先,它让学生接触到生产级别的代码库,这些代码往往经过多年的迭代和优化,包含了各种设计模式、架构决策和性能优化技巧。其次,学生需要学会阅读和理解他人代码,这是软件工程中极为重要但常被忽视的技能。最后,通过与社区成员的互动,学生能够学习到专业的沟通方式和协作流程,这些都是未来职业发展中不可或缺的软技能。

第一阶段:基础准备与认知建设

1.1 建立正确的开源认知体系

导师在引领学生之前,首先需要帮助他们建立对开源社区的正确认知。许多学生误以为开源贡献就是”免费劳动”,或者认为只有资深开发者才能参与。导师需要明确指出,开源贡献是一个双向学习过程:贡献者获得技能和经验,项目获得改进和活力。

认知建设的具体步骤:

  • 组织开源文化讲座,讲解开源许可证(MIT、GPL、Apache等)的基本概念
  • 分析知名开源项目的发展历程,如Linux内核、React、TensorFlow等
  • 讨论开源贡献对个人简历和技术声誉的积极影响
  • 强调”小贡献也是贡献”的理念,鼓励从简单任务开始

1.2 技术栈与工具链准备

在正式参与开源项目前,学生需要掌握必要的技术工具。导师应该设计一个系统的工具培训计划。

必备工具链:

  • Git与GitHub/GitLab:版本控制是开源协作的基础
  • 命令行操作:大多数开源项目开发都在终端环境下进行
  • 调试工具:如GDB、Chrome DevTools、IDE调试器等
  • 测试框架:理解单元测试、集成测试的概念

Git基础培训示例:

# 1. 配置Git身份(重要第一步)
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

# 2. Fork工作流程演示
# 步骤1:Fork目标项目到自己的GitHub账号
# 步骤2:克隆自己的Fork到本地
git clone https://github.com/YOUR_USERNAME/project-name.git

# 3. 添加上游仓库(用于同步原项目更新)
git remote add upstream https://github.com/ORIGINAL_OWNER/project-name.git

# 4. 创建特性分支
git checkout -b fix-typo-in-readme

# 5. 提交更改
git add .
git commit -m "docs: fix typo in README.md"

# 6. 推送到自己的Fork
git push origin fix-typo-in-readme

# 7. 创建Pull Request
# 在GitHub界面操作,填写详细的PR描述

1.3 选择合适的入门项目

导师需要根据学生的技能水平和兴趣,精心挑选适合的入门项目。一个好的入门项目应该具备以下特征:

  • 活跃的社区和快速的PR响应
  • 清晰的贡献指南(CONTRIBUTING.md)
  • 良好的issue标签系统(如”good first issue”、”help wanted”)
  • 适中的代码库规模,避免过于庞大或过于简单

项目评估清单:

- [ ] 项目是否在最近3个月内有活跃提交?
- [ ] 是否有明确的贡献指南?
- [ ] Issue列表中是否有适合新手的标签?
- [ ] 社区成员是否友好且响应及时?
- [ ] 项目文档是否完善?
- [ ] 是否有活跃的沟通渠道(Discord、Slack、邮件列表)?

第二阶段:实际贡献流程详解

2.1 Issue识别与任务分解

导师应该教会学生如何在开源项目中找到合适的任务。这需要培养学生的问题识别能力和任务评估能力。

Issue选择策略:

  1. 标签筛选法:优先查看带有”good first issue”、”beginner-friendly”、”documentation”等标签的issue
  2. 文档改进:从修复拼写错误、更新过时信息、补充缺失文档开始
  3. 测试覆盖:为缺少测试的函数添加单元测试
  4. 小功能实现:实现简单的UI改进或命令行参数

实际案例:为Python开源项目添加测试 假设学生发现一个Python工具库缺少对某个函数的测试:

# 原项目代码(假设)
def calculate_discount(price, discount_percent):
    """Calculate the final price after discount."""
    return price * (1 - discount_percent / 100)

# 学生需要添加的测试代码
import unittest
from mymodule import calculate_discount

class TestDiscountCalculation(unittest.TestCase):
    def test_normal_discount(self):
        """Test normal discount calculation."""
        result = calculate_discount(100, 20)
        self.assertEqual(result, 80)
    
    def test_zero_discount(self):
        """Test with zero discount."""
        result = calculate_discount(100, 0)
        self.assertEqual(result, 100)
    
    def test_full_discount(self):
        """Test with 100% discount."""
        result = calculate_discount(100, 100)
        self.assertEqual(result, 0)
    
    def test_invalid_inputs(self):
        """Test with invalid inputs."""
        with self.assertRaises(TypeError):
            calculate_discount("100", 20)

if __name__ == '__main__':
    unittest.main()

2.2 代码阅读与理解策略

面对陌生的代码库,学生往往感到无从下手。导师需要传授系统化的代码阅读方法。

代码阅读四步法:

  1. 宏观理解:通过README、架构文档了解项目目的和整体结构
  2. 入口定位:找到程序的入口点或核心模块
  3. 依赖分析:使用工具分析模块间的依赖关系
  4. 调试跟踪:通过实际运行和调试来理解代码流程

代码依赖分析工具示例:

# Python项目使用pydeps生成依赖图
pip install pydeps
pydeps myproject --show-deps --output deps.png

# JavaScript项目使用madge
npm install -g madge
madge --image deps.svg src/

# C/C++项目使用doxygen生成文档
doxygen Doxyfile

2.3 开发环境配置与问题排查

开源项目的环境配置往往是学生遇到的第一个障碍。导师应该提供详细的环境配置指导和常见问题解决方案。

环境配置检查清单:

# 通用环境检查脚本
#!/bin/bash
echo "=== 环境检查清单 ==="

# 1. 检查Python版本
python --version || python3 --version

# 2. 检查Node.js版本
node --version

# 3. 检查Git配置
git config --list | grep user

# 4. 检查虚拟环境
if [ -d "venv" ]; then
    echo "虚拟环境存在"
    source venv/bin/activate
else
    echo "建议创建虚拟环境: python -m venv venv"
fi

# 5. 检查依赖安装
if [ -f "requirements.txt" ]; then
    pip install -r requirements.txt
elif [ -f "package.json" ]; then
    npm install
fi

echo "=== 检查完成 ==="

常见环境问题解决方案:

  • 依赖冲突:使用虚拟环境隔离(Python的venv,Node的nvm)
  • 权限问题:避免使用sudo安装包,正确配置用户权限
  • 路径问题:使用相对路径,避免硬编码绝对路径
  • 版本不匹配:使用版本管理工具(pyenv、nvm)

2.4 编写高质量的提交信息

提交信息(Commit Message)是开源贡献中极其重要但常被忽视的部分。导师需要强调其重要性并提供模板。

Angular Commit规范示例:

<type>(<scope>): <subject>

<body>

<footer>

# 其中:
# type: feat, fix, docs, style, refactor, test, chore
# scope: 影响范围(可选)
# subject: 简短描述(不超过50字符)
# body: 详细描述(可选)
# footer: 关联issue(可选)

实际提交示例:

fix: 修复用户登录时的空指针异常

在用户登录过程中,当用户配置文件不存在时,会抛出NullPointerError。
通过添加空值检查来修复此问题。

修复了 #123 中报告的问题

相关测试:
- 添加了测试用例验证空配置文件的处理
- 确保现有测试仍然通过

第三阶段:深入参与与技能提升

3.1 代码审查与反馈处理

学会接受和处理代码审查反馈是开源贡献的重要技能。导师应该模拟审查过程,让学生提前适应。

代码审查要点:

  • 功能正确性:代码是否解决了问题?
  • 代码风格:是否符合项目规范?
  • 测试覆盖:是否有足够的测试?
  • 文档更新:是否需要更新相关文档?
  • 性能考虑:是否存在性能问题?

处理审查反馈的流程:

# 1. 获取最新的审查意见
git fetch upstream
git checkout your-branch

# 2. 根据反馈修改代码
# ... 编辑代码 ...

# 3. 提交修改(使用--amend保持提交历史整洁)
git add .
git commit --amend --no-edit

# 4. 强制推送更新
git push origin your-branch --force

# 5. 在PR页面回复审查者
# 例如:"感谢审查!已按建议修改了变量命名并添加了相关测试"

3.2 性能优化与调试技巧

当学生开始处理更复杂的issue时,导师需要教授性能分析和调试的高级技巧。

性能分析示例(Python):

import cProfile
import pstats
from io import StringIO

def slow_function():
    """一个需要优化的示例函数"""
    total = 0
    for i in range(1000000):
        total += i ** 2
    return total

# 使用cProfile分析性能
def profile_function():
    pr = cProfile.Profile()
    pr.enable()
    
    slow_function()
    
    pr.disable()
    
    # 输出性能报告
    s = StringIO()
    ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
    ps.print_stats()
    print(s.getvalue())

# 优化后的版本
def optimized_function():
    """使用数学公式优化"""
    n = 1000000
    return n * (n + 1) * (2 * n + 1) // 6

调试技巧示例(JavaScript):

// 使用Chrome DevTools进行性能调试
function performanceDebug() {
    // 1. 使用console.time()测量执行时间
    console.time('operation');
    
    // 模拟耗时操作
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
        sum += Math.sqrt(i);
    }
    
    console.timeEnd('operation'); // 输出: operation: 12.345ms
    
    // 2. 使用Performance API
    performance.mark('start-operation');
    
    // ... 执行操作 ...
    
    performance.mark('end-operation');
    performance.measure('operation-duration', 'start-operation', 'end-operation');
    
    const measures = performance.getEntriesByName('operation-duration');
    console.log(`Duration: ${measures[0].duration}ms`);
}

// 使用Chrome DevTools的Memory面板分析内存泄漏
function memoryDebug() {
    const data = [];
    
    // 模拟内存泄漏
    setInterval(() => {
        data.push(new Array(10000).fill('leak'));
    }, 1000);
    
    // 在DevTools中:
    // 1. 打开Memory面板
    // 2. 拍摄堆快照
    // 3. 对比快照找出泄漏对象
}

3.3 跨项目协作与网络建设

导师应该鼓励学生参与多个项目,建立开发者网络。这不仅扩展技术视野,也为未来职业发展铺路。

跨项目参与策略:

  • 技术栈关联:从熟悉的技术栈开始,逐步扩展
  • 社区关联:参与同一组织下的多个项目
  • 主题关联:关注特定领域(如机器学习、Web开发)的多个项目

建立开发者网络的方法:

  1. 积极回复:在issue和PR中积极回复他人问题
  2. 组织线上活动:参与或组织线上hackathon
  3. 撰写技术博客:分享开源贡献经验
  4. 参加社区会议:如PyCon、JSConf等

第四阶段:导师的具体引领策略

4.1 个性化学习路径设计

每个学生的技术背景和兴趣不同,导师需要设计个性化的学习路径。

学生评估矩阵:

| 技能维度     | 初级(1) | 中级(2) | 高级(3) | 评估结果 |
|-------------|---------|---------|---------|----------|
| 编程语言熟练度 | 基础语法 | 常用库   | 框架开发 |          |
| Git掌握程度   | 基本操作 | 分支管理 | 高级特性 |          |
| 调试能力     | print调试 | 断点调试 | 性能分析 |          |
| 文档能力     | 能读文档 | 能写文档 | 能设计文档结构 |          |
| 沟通能力     | 能提问   | 能讨论   | 能领导讨论 |          |

根据评估结果,为不同学生定制任务:

  • 初级学生:从文档改进、简单bug修复开始
  • 中级学生:实现小功能、添加测试、参与代码审查
  • 高级学生:负责模块开发、指导新手、参与架构讨论

4.2 定期代码审查与反馈循环

建立定期的代码审查机制,帮助学生快速成长。

审查会议流程:

# 每周代码审查会议流程
1. 学生展示本周贡献(10分钟)
   - 演示PR内容
   - 解释技术决策
   - 分享遇到的问题

2. 导师和同学提问(15分钟)
   - 代码质量反馈
   - 架构设计讨论
   - 最佳实践建议

3. 总结与改进计划(10分钟)
   - 本周亮点
   - 改进方向
   - 下周目标

代码审查检查表:

- [ ] 代码是否遵循PEP8/StandardJS等规范?
- [ ] 是否有适当的错误处理?
- [ ] 测试覆盖率是否足够?
- [ ] 文档是否同步更新?
- [ ] 提交信息是否清晰?
- [ ] 是否考虑了边界情况?
- [ ] 性能是否可接受?
- [ ] 代码是否易于理解?

4.3 激励机制与成就感培养

开源贡献周期长,导师需要设计激励机制保持学生动力。

阶段性成就系统:

  • 第一颗星:第一个PR被合并
  • 文档专家:连续贡献3个文档改进
  • 测试守护者:为模块增加50%测试覆盖率
  • Issue终结者:独立解决5个issue
  • 社区导师:帮助其他新手贡献者

可视化进度追踪:

# 简单的贡献追踪脚本
import requests
from datetime import datetime

def track_contributions(username, project):
    """追踪GitHub贡献"""
    url = f"https://api.github.com/repos/{project}/issues"
    response = requests.get(url)
    
    contributions = []
    for issue in response.json():
        if issue['user']['login'] == username:
            contributions.append({
                'title': issue['title'],
                'state': issue['state'],
                'created_at': issue['created_at']
            })
    
    return contributions

# 生成贡献报告
def generate_report(username, project):
    contribs = track_contributions(username, project)
    print(f"=== {username} 在 {project} 的贡献 ===")
    print(f"总贡献数: {len(contribs)}")
    print(f"已解决: {sum(1 for c in contribs if c['state'] == 'closed')}")
    
    for contrib in contribs:
        status = "✓" if contrib['state'] == 'closed' else "○"
        print(f"{status} {contrib['title']}")

4.4 处理挫折与心理支持

开源贡献过程中会遇到各种挫折,导师需要提供心理支持和解决方案。

常见挫折及应对策略:

挫折类型 具体表现 导师应对策略
PR被拒绝 代码不符合要求 分析拒绝原因,指导修改,强调这是学习过程
沟通障碍 语言或文化差异 提供沟通模板,帮助理解社区文化
技术瓶颈 问题超出当前能力 分解问题,提供学习资源,或建议换更简单的任务
时间冲突 学业与贡献平衡 制定合理计划,强调质量而非数量
缺乏反馈 PR长时间无人响应 教授如何礼貌地跟进,或协助联系维护者

心理支持话术示例:

"你的PR被要求修改是很正常的,即使是经验丰富的开发者也会收到很多反馈。
这说明维护者认真看了你的代码,这是很好的学习机会。
让我们一起分析这些反馈,看看如何改进。记住,每个优秀的贡献者都经历过这个过程。"

第五阶段:实际案例与完整流程演示

5.1 完整贡献案例:修复开源Web应用的Bug

让我们通过一个完整的案例,展示导师如何引领学生完成一个真实的开源贡献。

案例背景:

  • 项目:一个开源的TODO列表Web应用(React + Node.js)
  • 问题:用户报告在移动端无法删除任务
  • 学生水平:中级(熟悉React基础,但没做过开源贡献)

完整流程:

第1步:问题复现与分析

# 1. Fork并克隆项目
git clone https://github.com/YOUR_USERNAME/todo-app.git
cd todo-app

# 2. 安装依赖
npm install

# 3. 启动项目
npm start

# 4. 在浏览器中复现问题
# 使用Chrome DevTools的设备模拟器切换到移动端
# 尝试删除任务,观察问题

第2步:定位问题代码

// 在src/components/TaskList.js中找到删除相关代码
// 问题可能出现在:
// 1. 事件处理函数
// 2. 触摸事件与点击事件的兼容性
// 3. 样式问题(z-index或pointer-events)

// 常见的移动端删除问题代码:
const TaskItem = ({ task, onDelete }) => {
  return (
    <div className="task-item">
      <span>{task.text}</span>
      {/* 问题:移动端可能无法触发onClick */}
      <button onClick={() => onDelete(task.id)}>删除</button>
    </div>
  );
};

// 修复方案1:添加触摸事件处理
const TaskItem = ({ task, onDelete }) => {
  const handleDelete = (e) => {
    e.preventDefault();
    onDelete(task.id);
  };
  
  return (
    <div className="task-item">
      <span>{task.text}</span>
      <button 
        onClick={handleDelete}
        onTouchEnd={handleDelete}  // 添加触摸事件
      >
        删除
      </button>
    </div>
  );
};

// 修复方案2:使用React的合成事件(推荐)
import { TouchEvent } from 'react';

const TaskItem = ({ task, onDelete }) => {
  const handleTouchEnd = (e) => {
    e.preventDefault();
    // 等待一小段时间确保点击事件完成
    setTimeout(() => onDelete(task.id), 50);
  };
  
  return (
    <div className="task-item">
      <span>{task.text}</span>
      <button 
        onClick={() => onDelete(task.id)}
        onTouchEnd={handleTouchEnd}
      >
        删除
      </button>
    </div>
  );
};

第3步:编写测试验证修复

// src/components/TaskList.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import TaskItem from './TaskItem';

describe('TaskItem 移动端删除功能', () => {
  const mockDelete = jest.fn();
  const task = { id: 1, text: '测试任务' };
  
  beforeEach(() => {
    mockDelete.mockClear();
  });
  
  test('桌面端点击删除按钮应触发onDelete', () => {
    render(<TaskItem task={task} onDelete={mockDelete} />);
    
    const button = screen.getByText('删除');
    fireEvent.click(button);
    
    expect(mockDelete).toHaveBeenCalledWith(1);
  });
  
  test('移动端触摸删除按钮应触发onDelete', async () => {
    render(<TaskItem task={task} onDelete={mockDelete} />);
    
    const button = screen.getByText('删除');
    fireEvent.touchEnd(button);
    
    await waitFor(() => {
      expect(mockDelete).toHaveBeenCalledWith(1);
    });
  });
  
  test('应防止触摸事件的重复触发', async () => {
    render(<TaskItem task={task} onDelete={mockDelete} />);
    
    const button = screen.getByText('删除');
    
    // 快速多次触摸
    fireEvent.touchEnd(button);
    fireEvent.touchEnd(button);
    fireEvent.touchEnd(button);
    
    await waitFor(() => {
      expect(mockDelete).toHaveBeenCalledTimes(1);
    });
  });
});

第4步:准备提交

# 1. 运行测试确保通过
npm test

# 2. 运行lint检查
npm run lint

# 3. 查看修改文件
git status

# 4. 添加修改
git add src/components/TaskList.js src/components/TaskList.test.js

# 5. 编写详细的提交信息
git commit -m "fix: 修复移动端无法删除任务的问题

问题描述:
- 在iOS Safari和Android Chrome中,点击删除按钮无响应
- 用户反馈无法完成基本操作

解决方案:
- 添加onTouchEnd事件处理器兼容移动端
- 添加防重复触发机制
- 新增移动端专项测试用例

相关issue:Fixes #45"

第5步:创建Pull Request

# 1. 推送分支
git push origin fix-mobile-delete

# 2. 在GitHub上创建PR
# 标题:fix: 修复移动端无法删除任务的问题
# 描述:
# ## 问题
# 在移动端无法删除任务,用户无法完成基本操作。
# 
# ## 解决方案
# 1. 添加触摸事件处理
# 2. 防止重复触发
# 3. 添加测试
# 
# ## 测试
# - [x] 桌面端测试通过
# - [x] 移动端测试通过
# - [x] 新增测试用例通过
# 
# ## 截图
# [附上修复前后的对比截图]

# 3. 导师协助审查
# 导师在PR中评论:
# "很好!建议:
# 1. 考虑使用React的onPointerDown事件,更统一
# 2. 添加对键盘删除的支持(无障碍)
# 3. 50ms延迟可能需要根据实际测试调整"

第6步:处理审查反馈

// 根据导师建议改进
import { PointerEvent } from 'react';

const TaskItem = ({ task, onDelete }) => {
  const handleDelete = (e) => {
    e.preventDefault();
    onDelete(task.id);
  };
  
  // 使用onPointerDown统一处理鼠标和触摸事件
  // 并添加键盘支持
  return (
    <div className="task-item">
      <span>{task.text}</span>
      <button 
        onPointerDown={handleDelete}
        onKeyDown={(e) => {
          if (e.key === 'Delete' || e.key === 'Backspace') {
            handleDelete(e);
          }
        }}
        aria-label={`删除任务: ${task.text}`}
      >
        删除
      </button>
    </div>
  );
};

第7步:合并后的跟进

# 1. 同步上游更新
git checkout main
git fetch upstream
git merge upstream/main
git push origin main

# 2. 删除已合并的分支
git branch -d fix-mobile-delete
git push origin --delete fix-mobile-delete

# 3. 在issue中感谢维护者
# "感谢快速审查和有价值的建议!已按反馈修改。
# 学到了很多关于移动端事件处理和无障碍设计的知识。"

# 4. 分享经验
# 在团队博客或技术社区分享这次贡献经历

5.2 导师在每个阶段的具体指导动作

阶段1:准备阶段

  • 动作:与学生一对一会议,评估技能水平,推荐3-5个候选项目
  • 输出:个性化项目选择建议书
  • 时间:1-2周

阶段2:首次贡献

  • 动作:每周2次代码审查会议,实时解答Git问题,协助撰写PR描述
  • 输出:第一个被合并的PR
  • 时间:2-3周

阶段3:独立贡献

  • 动作:减少直接指导,增加架构讨论,鼓励学生帮助其他新手
  • 输出:3-5个独立贡献,开始参与代码审查
  • 时间:1-2个月

阶段4:社区核心

  • 动作:协助学生成为某个模块的维护者,推荐会议演讲机会
  • 输出:成为项目贡献者,可能获得committer权限
  • 时间:3-6个月

第六阶段:评估与持续改进

6.1 贡献质量评估体系

建立科学的评估体系,帮助学生了解自己的进步。

评估维度:

1. **技术能力** (40%)
   - 代码质量
   - 问题解决效率
   - 测试覆盖率

2. **协作能力** (30%)
   - PR响应速度
   - 沟通质量
   - 帮助他人次数

3. **社区影响力** (20%)
   - 被合并的PR数量
   - Review他人代码次数
   - 文档贡献

4. **学习能力** (10%)
   - 技术博客
   - 新技术掌握速度

6.2 持续改进机制

每月回顾会议:

1. **成就回顾**
   - 本月完成的贡献
   - 获得的正面反馈
   - 技术能力提升点

2. **问题分析**
   - 遇到的主要困难
   - 未解决的问题
   - 需要支持的领域

3. **目标设定**
   - 下月具体目标
   - 需要学习的新技能
   - 计划参与的项目

4. **导师反馈**
   - 观察到的进步
   - 改进建议
   - 资源推荐

反馈循环模板:

# 学生自评模板
monthly_review = {
    "本月贡献": [],
    "技术收获": [],
    "遇到的挑战": [],
    "需要帮助的领域": [],
    "下月目标": []
}

# 导师评估模板
mentor_feedback = {
    "观察到的进步": [],
    "代码质量评分": 0,
    "协作能力评分": 0,
    "具体改进建议": [],
    "推荐资源": []
}

结论:开源贡献的长期价值

通过系统化的引领,编程导师能够将开源贡献转化为学生成长的强大引擎。这不仅提升了技术能力,更重要的是培养了现代软件开发所需的核心素质:自主学习、协作沟通、问题解决和社区意识。

关键成功因素:

  1. 耐心与持续支持:开源贡献是马拉松,不是短跑
  2. 个性化指导:根据学生特点调整策略
  3. 正向激励:及时肯定小进步,建立信心
  4. 真实参与:导师自己也要积极参与开源,以身作则
  5. 社区连接:帮助学生建立有价值的开发者网络

最终目标: 让学生从”开源贡献的新手”成长为”开源社区的活跃成员”,甚至”开源项目的维护者”。这个过程不仅为他们的简历增添了亮点,更重要的是培养了终身学习和持续贡献的习惯,这将使他们在未来的职业生涯中持续受益。

开源贡献的教育价值在于它创造了一个真实的学习环境,在这里,学生不仅学习如何编程,更学习如何成为全球开发者社区的一员。作为导师,我们的使命就是搭建这座桥梁,引领学生踏上这段充满挑战与收获的旅程。