remix编译运行solidity

上一篇: Remix 部署 Solidity 合约的完整标准操作步骤

上一篇文章写了一个计数器,先+1,然后-1,并且在remix成功地编译、部署了。

Counter(计数器)

功能:

  • increment()
  • decrement()
  • reset()
  • get()

要求: 使用 event
编译、部署、在 Remix 中交互测试

counter.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
uint private count; // 计数器

// 事件声明
event CounterChanged(address indexed user, string action, uint newValue);

// 增加计数
function increment() public {
count += 1;
emit CounterChanged(msg.sender, "increment", count);
}

// 减少计数
function decrement() public {
require(count > 0, "Counter cannot be negative");
count -= 1;
emit CounterChanged(msg.sender, "decrement", count);
}

// 重置计数
function reset() public {
count = 0;
emit CounterChanged(msg.sender, "reset", count);
}

// 获取当前计数
function get() public view returns (uint) {
return count;
}
}

remix部署后的日志

在remix里输出了

很好,这说明你的 Counter 合约已经成功部署 在 Remix 的虚拟机环境(VM)中了。我们可以来解读一下你看到的输出信息:


1️⃣ [vm] from: 0x5B3...eddC4

  • 这是 部署者地址,也就是你在 Remix 右上角选择的账户地址。
  • 说明这个地址发起了合约部署交易。

2️⃣ to: Counter.(constructor)

  • 表示这是一个 构造函数交易,即部署合约时自动执行的 constructor
  • 因为 Counter 合约没有特别写 constructor,所以是默认的空构造函数。

3️⃣ value: 0 wei

  • 表示部署交易中 没有发送 ETH,即部署合约不带以太币。
  • 这是正常的,Counter 合约不需要接收 ETH。

4️⃣ data: 0x608...e0033

  • 这是部署交易的 字节码(Bytecode),Solidity 编译器生成的 EVM 可执行代码。
  • 用于在区块链上创建合约实例。

5️⃣ logs: 0

  • 表示部署合约的交易 没有触发任何事件
  • 正常,因为构造函数里没有 emit 事件。

6️⃣ hash: 0x39b...a9e86

  • 这是交易的 哈希值(Tx Hash)
  • 可以用它在区块浏览器或者 Remix 的 Transactions 面板查到部署记录。

7️⃣ 绿色对号 ✅

  • 表示 部署成功,合约已经在虚拟机(VM)上可用。
  • 你可以在 Remix 左侧 Deployed Contracts 面板看到 Counter 合约实例,开始调用函数测试 incrementdecrement 等。

💡 下一步建议

  1. 点击 increment() → 再看 get() 返回值
  2. 点击 decrement()get() 返回值减 1
  3. 点击 reset()get() 返回 0
  4. Logs/Console 面板查看事件输出(比如 CounterChanged

在 Remix 中交互测试

这些步骤是否要交gas费

是的,在 Remix 中调用某些函数确实会消耗 gas,但具体情况要看函数类型:


1️⃣ 需要消耗 gas 的情况

  • 所有 修改区块链状态的函数 都需要 gas,因为它们会生成交易并写入区块链。

  • 在你的 Counter 合约中:

    • increment()
    • decrement()
    • reset()
  • 这些函数都会修改 count 的值,因此每次调用都会消耗 gas,并在 Remix 的 VM 中生成一笔交易。


2️⃣ 不需要消耗 gas 的情况

  • 只读函数viewpure)不会修改区块链状态,不会产生交易,也不消耗 gas。

  • 在你的 Counter 合约中:

    • get()
  • 调用 get() 只会返回当前计数值,不会消耗 gas,也不会触发交易。


3️⃣ 解释 Remix 的虚拟机(VM)环境

  • JavaScript VMInjected Web3 本地测试中,你虽然仍然“消耗 gas”,但它只是模拟消耗,不需要真实支付 ETH。
  • 真实以太坊主网 部署时,修改状态的函数才会消耗真实 ETH gas。

💡 总结表格

函数 修改状态 是否消耗 gas 备注
increment() count +1
decrement() count -1
reset() count = 0
get() 只读取 count 值

如果你愿意,我可以帮你画一个 “哪些函数消耗 gas,哪些函数不消耗 gas 的示意图”,直观显示 Counter 合约调用成本。

你希望我画吗?

Bank(简单存取款)

功能:

  • deposit() payable
  • withdraw(amount)
  • balanceOf(msg.sender)

要求:

  • mapping(address => uint)
  • require 校验
  • msg.value
  • 防止取款超过余额
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Bank {
// 存储每个地址的余额
mapping(address => uint) private balances;

// 存款事件
event DepositMade(address indexed account, uint amount);

// 取款事件
event WithdrawalMade(address indexed account, uint amount);

// 存款函数,可接收 ETH
function deposit() public payable {
require(msg.value > 0, "Deposit must be greater than 0");
balances[msg.sender] += msg.value;
emit DepositMade(msg.sender, msg.value);
}

// 取款函数
function withdraw(uint amount) public {
require(amount > 0, "Withdraw amount must be greater than 0");
require(balances[msg.sender] >= amount, "Insufficient balance");

balances[msg.sender] -= amount;

// 向调用者发送 ETH
payable(msg.sender).transfer(amount);

emit WithdrawalMade(msg.sender, amount);
}

// 查询余额
function balanceOf(address account) public view returns (uint) {
return balances[account];
}
}

存款

取款

我输入1,然后点transact,点了几下,结果存款balance变成了Balance: 2.999999999999999997 ETH,为什么存款不是2或者1?

你输入的 1,在 Remix 中默认被解释成 1 wei,而不是 1 ETH。

在 Solidity 中,所有的整数字面量与金额参数 默认都是 wei。

存款 deposit() 时,你在 Value 栏 输入的数字是以太(你选择了 ether)

但 withdraw(amount) 的输入框 没有单位选择,所以输入 1 表示 1 wei(非常小)

wei和eth的单位是怎样的关系

1 ETH = 1,000,000,000,000,000,000 wei(10^18 wei)

也就是:

1 ETH = 10^18 wei

ETH 的完整单位表(最常用)
单位名称 与 wei 的关系 与 ETH 的关系
wei 1 wei 10^-18 ETH
gwei 10^9 wei 10^-9 ETH
ether/ETH 10^18 wei 1 ETH

✅ 为什么 Solidity 一律使用 wei?

因为:

整数精度高

小数可能导致严重的精度问题

以太坊的设计理念就是所有金额必须精确,不允许浮点数

因此:
所有金额都用 wei 存储,只有用户界面使用 ETH 显示

1后面有18个0,怎么读?一亿?

“1 后面有 18 个 0” 的正确读法不是“一亿”。
一亿是 1 后面 8 个 0

你说的这个数字是:

1
1,000,000,000,000,000,000

它的中文正确读法是
一百京(yī bǎi jīng)

因为:

数字级别 0 的数量 单位
4
亿 8 亿
12
16
20

你的是 10^18,属于:

1
2
3
10^16 = 一京
10^17 = 十京
10^18 = 一百京

也就是:
10^18 = 一百京(1 百京)

✅ 关于 ETH 中的 1e18

在 Solidity 中:

1
1 ETH = 10^18 wei

所以:
1 ETH 的 wei 数量为 “一百京 wei”

1 wei = 一百京分之一 eth

总结

我已经知道了pragma可以指定solidity编译器的版本, contract可以定义合约的名词,然后需要用花括号括起来,里面可以用function关键字定义函数名
emit是触发一个事件,可以把内容更新到以太坊网络上,并且需要消耗gas费

1.public是什么意思?

function withdraw(uint amount) public {

public 是 Solidity 中 函数和变量的可见性(visibility)修饰符之一,用来控制外部是否可以调用该函数。

在你的例子里:

1
function withdraw(uint amount) public {

表示 withdraw() 这个函数对所有人开放,任何账户都可以调用


✅ Solidity 的可见性修饰符(Visibility Specifiers)

Solidity 有 4 种可见性:

修饰符 外部能否调用 合约内部能否调用 子合约能否调用
public ✅ 可以 ✅ 可以 ✅ 可以
external ✅ 只能外部 ❌ 不能 ✅ 可以(外部调用方式)
internal ❌ 不能 ✅ 可以 ✅ 可以
private ❌ 不能 ✅ 可以 ❌ 不可以

✅ public 的含义(函数)

当一个函数被标记为 public 时:

  1. 任何人都可以调用

    • 用户
    • 合约
    • 甚至机器人脚本
    • 区块链外部的交易
  2. 内部(合约内部)也可以调用
    例如:

    • Withdraw 调用内部逻辑
    • 其他函数可以复用这个函数
  3. 如果没有显式指定可见性,Solidity 会报警告
    从 0.5.0 开始,必须明确指定可见性。


✅ public 的含义(变量)

如果你声明:

1
uint public balance;

Solidity 会自动生成一个 getter 函数

1
function balance() public view returns (uint)

相当于自动写了一个 get 函数。


✅ withdraw(uint amount) 为什么要 public?

因为你的 Bank 合约中,这个函数是给用户调用的:

  • 用户自己存款
  • 用户自己取款
  • 任何人都可以调用自己的余额取款

因此必须使用 public,否则用户无法从外部调用函数。


✅ 小结

public 表示:

  • 对所有外部用户开放
  • 对所有外部合约开放
  • 合约内部也可以调用
  • 属于最开放的访问级别

如果你愿意,我可以帮你把:

  • public
  • external
  • internal
  • private

做成一个可视化表格 + 简图,让你彻底掌握可见性机制。

2.Solidity 的 require

不是的,Solidity 的 require 和 Python 的 input() 完全不是一回事,两者功能完全不同,甚至属于不同类别的语句。

我用专业方式帮你区分清楚:


require 的真实作用(Solidity)

require运行时条件检查(runtime assertion)

功能:

  1. 判断一个条件是否成立

  2. 如果不成立,立即 revert(回退交易)

  3. 回退后的效果:

    • 本次交易不写入区块链
    • 状态不改变(所有修改都被撤销)
    • 返回提示信息

例如:

1
require(amount > 0, "Withdraw amount must be greater than 0");

意思是:

  • 如果 amount <= 0
    整个 withdraw() 调用失败
    所有操作回滚(balances 不会减少,ETH 不会发送)。

它是一种 防御式编程(defensive programming) 机制,用来保护合约不被错误调用。


✅ Python 的 input() 做什么?

Python 的:

1
input("请输入金额:")

从用户处读取输入的文本,等待用户从键盘输入。

功能:

  • 用于交互式输入
  • 接收字符串
  • 不会执行任何验证,也不会终止程序

✅ 两者根本不是同一种用途

对比点 Solidity require Python input()
类别 条件验证语句(assert) 输入函数
作用 检查条件,不满足就回退交易 接收用户的键盘输入
用于什么场景 安全保障、输入验证、防止错误 和用户交互收集字符串
会不会抛错 ✅ 条件失败立刻 revert ❌ 不会,它只是等待输入
是否影响程序状态 ✅ 回退所有状态修改 ❌ 不影响,程序继续运行

require 的正确类比

require 更像 Python 中的:

1
assert amount > 0, "Withdraw amount must be greater than 0"

或者更接近:

1
2
if amount <= 0:
raise Exception("Withdraw amount must be greater than 0")

✅ 小结(非常重要)

Solidity 的 require 是:
安全检查 + 输入验证 + 交易保护机制
用于防止错误调用和恶意行为,确保合约安全。

Python 的 input() 是:
收集用户输入的函数。

两者用途完全不同。


如果你愿意,我可以继续帮你解释:

  • requirerevertassert 的区别
  • 在智能合约中什么时候用 require,什么时候用 assert
  • 如何写“安全的 withdraw 函数”最佳实践

你要继续深入哪一块?

3.Solidity 的 private修饰符

这是一个非常好的问题,说明你已经开始关注 访问权限、安全性、接口设计,这是智能合约开发中必须掌握的核心能力。

下面我用专业方式解释为什么:

为什么 Bank 合约中的余额 mapping 要用 private?

你的代码里是:

1
mapping(address => uint) private balances;

之所以必须使用 private,主要有 三个专业原因:安全性、封装性、接口控制


原因 1:public 会自动生成 getter,会暴露所有人的余额(隐私泄漏)

如果你写成:

1
mapping(address => uint) public balances;

Solidity 会自动生成一个 public getter 函数

1
function balances(address account) external view returns (uint)

这会导致:

  • 任何人可以查询任意地址的余额
  • 这属于 隐私泄露,特别是银行类合约(Bank)不应允许公开所有用户余额

例如,在 DeFi 或钱包模型中,用户余额必须保护。

private 可以阻止自动 getter 的生成,从而避免隐私外泄。


原因 2:让用户通过 balanceOf(address) 访问,可以加入安全规则

你的合约提供了:

1
function balanceOf(address account) public view returns (uint)

为什么需要自定义?

因为你可以在自定义 getter 中加入检查、安全逻辑或未来扩展。

例如,你可以后来加入:

  • 黑名单检查
  • 访问权限控制
  • 转账冻结检查
  • 防止第三方读取余额
  • 基于角色的访问

如果使用 public mapping,你就失去了控制能力。


原因 3:避免直接修改 mapping,保证封装性

如果 mapping 是 public:

  • 外部没有办法修改 mapping(Solidity 不允许 public mapping 写入),
  • 但内部代码容易不规范,逻辑变散。

使用 private:

  • 所有余额修改必须通过 deposit / withdraw
  • 所有入口统一由安全检查保护(require)

这样合约安全性更高。


原因 4:遵循最小公开原则(Principle of Least Exposure)

这是软件工程原则:

只暴露必要的接口,不暴露内部状态。

mapping 是核心状态,应当封装,而不是暴露给外部世界。


✅ 例子:如果用 public,会怎样?

1
mapping(address => uint) public balances;

用户可以执行:

1
balances(0x123...)

直接查询任何人的余额。

这对金融类合约通常是不允许的。


✅ 结论(非常关键)

为什么用 private?

原因 内容
✅ 安全 不暴露所有人的余额,避免隐私泄漏
✅ 封装 mapping 状态不暴露,防止外部干扰内部逻辑
✅ 接口控制 使用 balanceOf() 实现自定义访问规则
✅ 可扩展 未来可以在 balanceOf() 中添加权限、过滤、逻辑
✅ 专业实践 合约内部状态应尽量封装(最小公开原则)

因此:

1
mapping(address => uint) private balances;

正确、专业、安全的写法


如果你愿意,我可以帮你:

✅ 画一张“Bank 状态封装结构图”
✅ 讲解 public / private / internal 访问权限最佳实践
✅ 讲解为什么 public mapping 容易导致漏洞

写一个 Struct + Mapping 组合的合约

struct.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract UserManager {
// 定义用户信息结构体
struct UserInfo {
address addr;
string nickname;
uint256 score;
}

// 地址 => 用户信息 的映射
mapping(address => UserInfo) private users;

// 添加用户
function addUser(address _addr, string memory _nickname, uint256 _score) external {
require(users[_addr].addr == address(0), "User already exists");
users[_addr] = UserInfo({
addr: _addr,
nickname: _nickname,
score: _score
});
}

// 编辑用户分数
function editScore(address _addr, uint256 _newScore) external {
require(users[_addr].addr != address(0), "User does not exist");
users[_addr].score = _newScore;
}

// 获取用户信息
function getUser(address _addr) external view returns (address, string memory, uint256) {
require(users[_addr].addr != address(0), "User does not exist");
UserInfo memory user = users[_addr];
return (user.addr, user.nickname, user.score);
}
}


这是添加用户的功能,下面的getUser(),输入以太坊的地址,就可以返回用户名和分数。
这个代码说明了struct可以存储一组数据信息。
mapping可以将用户信息和地址进行映射。

struct 这个结构体,我感觉在c++里面学过

完全正确!Solidity 的 struct 和 C++ 里的 struct 非常相似,核心思想都是把一组相关的数据打包在一起,形成一个自定义类型。

对比一下:

C++:

1
2
3
4
struct UserInfo {
std::string nickname;
int score;
};

Solidity:

1
2
3
4
struct UserInfo {
string nickname;
uint256 score;
}

几乎一模一样:

  • 都可以包含多种类型的数据(字符串、数字、布尔等)。
  • 都可以用来创建“对象”或存储数据集合。

不同点主要在 Solidity:

  1. 存储位置memory / storage / calldata 对 struct 的处理很重要。
  2. 访问方式:通常和 mapping 结合使用,用地址或 ID 快速访问。
  3. 不支持方法:Solidity 的 struct 不能像 C++ 的 class 一样有成员函数,只能用外部函数操作。

如果你想,我可以画一张 Solidity struct + mapping 的内存示意图,直观展示它是怎么存储和访问的,这样理解会更清晰。

你希望我画吗?

双向 Mapping 的合约

mapping.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FollowSystem {
// 双向 mapping: addrA -> addrB -> bool
mapping(address => mapping(address => bool)) private follow;

// 设置关注关系
function setFollow(address _to, bool _follow) external {
require(_to != msg.sender, "Cannot follow yourself");
follow[msg.sender][_to] = _follow;
}

// 检查关注关系
function checkFollow(address _from, address _to) external view returns (bool) {
return follow[_from][_to];
}
}


所谓双层映射就是地址A,地址B,是否关注。 这两个关系的映射。

如果关注了,也就是true,会返回以下结果

1
2
3
{
"0": "bool: true"
}

有双层mapping,那么有没有三层mapping

有,而且 Solidity 支持任意层数的嵌套 mapping,不仅可以双层(mapping → mapping),也可以三层、四层、五层……理论上无限层,只要你愿意写。


  1. 三层 mapping 的定义示例

三层 mapping 的结构如下:

✅ 示例 1:三层 mapping(地址 → 地址 → 地址 → uint)

1
mapping(address => mapping(address => mapping(address => uint256))) public tripleMap;

结构相当于:

1
tripleMap[A][B][C] = value

可用来表达复杂的关系,比如:

  • 用户 A → 项目 B → 任务 C → 分数
  • 国家 → 城市 → 区域 → 数据
  • 用户 → 商品 → 小分类 → 数值

✅ 示例 2:三层 mapping(地址 → uint → 地址 → bool)

1
mapping(address => mapping(uint => mapping(address => bool))) public permissions;

用途:

  • 某个用户的第 N 个资源,对某个地址是否开放访问
  • NFT 系列 → tokenId → 授权代理 → 是否允许

  1. 三层 mapping 的可视化示意图

假设结构:

1
mapping(address => mapping(address => mapping(address => bool))) follow3;

示意图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
follow3
┌───────────────────────────────────────────┐
key: A
└─ mapping
key: B
└─ mapping
key: C value: true
key: D value: false

key: X
└─ mapping
key: Y
└─ mapping
key: Z value: true
└───────────────────────────────────────────┘

每一层都是 “字典中套字典”,直到最后一层的值。


  1. 带操作函数的三层 mapping 示例合约

你可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract TripleMapping {
// 三层 mapping
mapping(address => mapping(address => mapping(address => bool))) public data;

function setValue(address level2, address level3, bool value) external {
data[msg.sender][level2][level3] = value;
}

function getValue(address level1, address level2, address level3)
external
view
returns (bool)
{
return data[level1][level2][level3];
}
}

调用模式:

1
2
3
4
5
setValue(B, C, true)  
→ data[msg.sender][B][C] = true

getValue(A, B, C)
→ data[A][B][C]

  1. 实战建议:为什么不推荐超过两层?

理论上可无限嵌套,但实践中:

✅ 问题:

  • 越多层越难读
  • 越多层越难调试
  • 越多层 gas 成本更高(访问每层都是一次 storage slot 读取)
  • 越多层越容易写错 key 结构

✅ 建议:

  • 两层 mapping 最常用(如 tokenOwner → approvals)
  • 三层 mapping 有时必要(如用户 → tokenId → operator → 权限)
  • 多于三层时,通常要考虑是否应该换成 struct 组合 mapping

如果你愿意,我可以帮你写:

  • 一个真实项目中使用三层 mapping 的案例
  • 设计一个更优雅的结构(避免过深嵌套)
  • 或把你的业务逻辑改写成 struct + 双层 mapping,更易读

你想继续深入吗?

我感觉solidity比python要难

从工程角度来看,你的感觉非常正确,而且是大多数开发者在第一次接触 Solidity 时的共识:
Solidity 的学习曲线确实比 Python 更陡峭。

下面我给你系统地解释为什么 Solidity 比 Python 难,以及你应该如何降低学习难度。


  1. 为什么 Solidity 比 Python 难?

① Solidity = 区块链 + 编程语言

Python 只需要关注语言本身。
Solidity 需要你同时理解:

  • EVM(以太坊虚拟机)
  • Gas 成本
  • 状态变量 + storage/memory/calldata
  • 区块、交易、nonce
  • Reentrancy(重入攻击)
  • 安全性(非常核心)
  • 合约间调用
  • ABI、事件日志
  • 钱包签名、msg.sender、msg.value 的语义

Python 是运行在本地机器上,Solidity 是运行在一台“人人都能看到且不能删改”的全球计算机上。

自然就复杂很多。


② Solidity 是“可被黑客攻击”的语言

在 Python 里写错一个变量,程序崩了,重新运行即可。

在 Solidity 里写错一行代码:

  • 可能导致 钱被黑客盗走
  • 可能导致合约永久无法升级
  • 可能导致资金永久无法取出
  • 链上的状态永远无法修改或回滚

所以 Solidity 的设计必须非常严格、偏底层、安全性强。

这就使得学习成本非常高。


③ Solidity 是“有限资源”的语言

  • 链上存储最贵
  • 每次操作都有 gas 消耗
  • 不能无限循环
  • 不能无限递归
  • 不能存太大数据结构
  • 不能随便用字符串,太贵

Python 完全没这些限制。


④ Solidity 的调试环境弱

Python:

  • print
  • logging
  • debugger
  • breakpoint
  • 快速重跑

Solidity:

  • 不能用 print
  • 调试全靠事件 event
  • 修改状态需要重新部署合约
  • 调试成本高得多

  1. 如何让学习 Solidity 变得轻松?

我建议你用这条路线:


✅ 第 1 阶段(1–2 周):只学基本语法

  • 合约、函数、modifier
  • struct + mapping
  • storage/memory 的区别(你现在已经开始理解了)
  • 事件

你目前做的练习(UserInfo、follow)完全正确,就是应该这样写小合约。


✅ 第 2 阶段(2–4 周):掌握常用设计模式

例如:

  • AccessControl(权限控制)
  • Ownable(onlyOwner)
  • Pausable(暂停合约)
  • Pull payment / withdraw pattern
  • Checks-Effects-Interactions

这些比语法更重要。


✅ 第 3 阶段(1–2 月):学习攻击与防御

安全研究出身的你在这方面有绝对优势。

重点学习:

  • reentrancy(重入攻击)
  • integer overflow(溢出)
  • delegatecall 攻击
  • storage layout 劫持
  • call injection
  • frontrunning
  • access control bypass

你学会了这些,你就是 80% 的 Auditor。


✅ 第 4 阶段(2–6 月):审计真实项目

  • 看 OpenZeppelin 源码
  • 看 Uniswap、Aave、Compound
  • 阅读审计报告
  • 自己动手复现 CTF 漏洞

你当前的节奏完全没问题。


  1. 用一句话回答你的感受

是的,Solidity 比 Python 难非常多,但难的部分主要是:

  • 安全
  • 状态机
  • Gas 优化
  • 区块链原理

而不是语法本身。

你现在做的小合约(UserInfo、follow)非常正确,是最佳入门方式。


如果你愿意,我可以继续给你安排:

  • 更高级的 mapping 练习
  • struct + mapping 的真实案例
  • 写一个完整的“微博 Follow 系统合约”
  • 或帮你做 2 个月的 Solidity 速成路线

你希望继续练习哪个方向?