本文介绍了如何在 Cairo 中实现类似 Solidity 中 internal、private 和 pure 函数的功能。
Cairo 没有像 Solidity 那样的 "internal" 和 "pure" 修饰符(或者任何其他修饰符)。
回想一下,用 #[abi(embed_v0)] 标记一个 impl 块会告诉 Cairo 将其函数包含在合约的 ABI (应用程序二进制接口) 中,从而使它们可以从合约外部调用。此 impl 块中的函数必须在它实现的 trait 中定义(类似于 Solidity 中的接口);这确保了外部调用者确切地知道哪些函数可用以及如何调用它们。
但是那些不应该从外部调用的函数呢?Cairo 能够限制函数可以做什么和不可以做什么,以及它们的功能可见性,就像 Solidity 一样。
在本文中,我们将展示如何在 Cairo 中实现等效于 internal、private 和 pure 函数的功能。
我们的第一个演示是一个 internal view 函数,它可以查看合约状态,但不能在合约外部调用。
要开始演示,创建一个名为 internal_demo 的空文件夹,并在其中运行 scarb init 以初始化一个新的 Cairo 项目。
接下来,在 src/lib.cairo 中添加一个函数 get_balance_2x(),如下所示:
// IHelloStarknet INTERFACE
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// ... 现有的函数 (increase_balance, get_balance) ...
// 新添加的函数
// 注意:这个函数会抛出异常
fn get_balance_2x(self: @ContractState) -> felt252 {
self.balance.read() * 2
}
}
}
我们得到一个编译错误,因为 get_balance_2x 不是 IHelloStarknet 的一部分。

如前所述,在 Cairo 中实现 trait 时,impl 块只能包含在该 trait 中定义的函数。不属于该 trait 的函数必须在单独的 impl 块中定义。这与 Solidity 不同,在 Solidity 中,合约可以自由地添加超出它们实现的接口中的函数。
但是,我们特别不希望将 get_balance_2x 包含在 IHelloStarknet trait 中,因为这会使该函数公开。
通过将 get_balance_2x 包含在 HelloStarknetImpl 块中(而不将其添加到 trait 中)而导致的编译错误的解决方案是:
get_balance_2x 放在单独的 impl 块中impl 块使用单独的 trait 。在 HelloStarknet 合约模块中的 HelloStarknetImpl 实现之后添加以下代码:
// 新添加的 //
#[generate_trait]
impl InternalFunction of IInternal {
fn get_balance_2x(self: @ContractState) -> felt252 {
self.balance.read() * 2
}
}
}
完整的合约如下所示,新添加的 InternalFunction impl 块以红色高亮显示:

InternalFunction 是完全任意的;它可以是对于合约有意义的任何名称。impl 块都需要一个关联的 trait,因此我们将其命名为 IInternal(也是任意的)。impl 创建 trait。编译器使用 #[generate_trait] 属性自动生成它。现在,如果我们尝试从测试 (tests/test_contract.cairo) 访问 get_balance_2x,
#[test]
fn test_balance_2x() {
let contract_address = deploy_contract("HelloStarknet");
let dispatcher = IHelloStarknetDispatcher { contract_address };
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
dispatcher.increase_balance(42);
let balance_after = dispatcher.get_balance_2x();
assert(balance_after == 42, 'Invalid balance');
}
测试将无法编译,因为该函数不是公开可见的:

为了测试 internal 函数是否按预期工作,我们将添加另一个函数 extern_wrap_get_balance_2x,它将是公共的,然后通过 self 变量访问我们的 internal 函数,如下所示。
不要忘记我们还需要将此函数添加到接口(如下面的红色框中所示),因为我们希望它可以从合约外部访问:

extern_wrap_balance_2x 函数(蓝色框)调用返回当前余额两倍的 internal 函数(绿色框)。这是完整的代码:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
/// Retrieve 2x the balance
fn extern_wrap_get_balance_2x(self: @TContractState) -> felt252;
}
/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
fn extern_wrap_get_balance_2x(self: @ContractState) -> felt252 {
self.get_balance_2x()
}
}
#[generate_trait]
impl InternalFunction of IInternal {
fn get_balance_2x(self: @ContractState) -> felt252 {
self.balance.read() * 2
}
}
}
将以下测试添加到 tests/test_contract.cairo 中现有测试的下方:
#[test]
fn test_balance_2x() {
// 部署 HelloStarknet 合约
// 注意:deploy_contract 是来自测试设置的辅助函数
let contract_address = deploy_contract("HelloStarknet");
// 创建一个 dispatcher 来与合约交互
let dispatcher = IHelloStarknetDispatcher { contract_address };
// 检查初始余额是否为 0
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
// 将余额增加 1
dispatcher.increase_balance(1);
// 调用使用我们的 internal 函数的包装函数
// 应该返回 1 * 2 = 2
let balance_after_2x = dispatcher.extern_wrap_get_balance_2x();
assert(balance_after_2x == 2, 'Invalid balance');
}
使用 scarb test test_balance_2x 运行测试;你应该看到它通过了。
总而言之:Cairo 中的 internal 函数是通过在没有 #[abi(embed_v0)] 的单独 impl 块中定义它们并使用 #[generate_trait] 自动生成 impl 块实现的 trait 来创建的。这使函数可以在你的合约中调用,但对外部调用者隐藏。
在 Solidity 中,“private” 函数和 “internal” 函数的区别在于子合约可以看到 “internal” 函数,但 “private” 函数只能被包含该函数的合约看到。
Cairo 没有继承,因此我们在 Cairo 中引用 “private” 函数时必须小心。
但是,一个自然的问题出现了:是否可以“模块化”函数的可视性?例如,在 Solidity 中,假设我们有以下设置:
contract A {
function private_magic_number() private returns (uint256) {
return 6;
}
function internal_mul_by_magic_number(uint256 x) internal returns (uint256) {
return x * private_magic_number()
}
}
contract B is A {
function external_fun() external returns (uint256) {
return internal_mul_by_magic_number();
}
}
合约 B 可以“看到”函数 internal_mul_by_magic_number(),因为它继承了 A;B 看不到 private_magic_number()。
但是,B 中的 external_fun() 在调用 internal_mul_by_magic_number() 时“在幕后” 使用 private_magic_number()。
让我们在 Cairo 中创建一个相同的构造,以展示一个函数如何像 Solidity 中的 private 函数一样对代码的其他部分不可用。
到目前为止,我们只将 mod(“模块”)视为合约函数的“容器”。但是,Cairo 允许我们使用嵌套模块进行进一步的模块化。我们可以使用这种模式来实现类似于 Solidity 中 private 函数的功能。
下面是当你生成一个新的 Scarb (snfoundry) 项目时的默认合约结构,但是有一个内部 mod,其中包含函数 internal_mul_by_magic_number() 和 private_magic_number()。
这个内部模块是在合约末尾声明的,因此你可以直接滚动到那里以查看关键更改:
/// Interface representing `HelloContract`.
/// This interface allows modification and retrieval of the contract balance.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: felt252);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> felt252;
fn get_balance_6x(self: @TContractState) -> felt252;
}
/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
fn get_balance_6x(self: @ContractState) -> felt252 {
rare_library::internal_mul_by_magic_number(self.balance.read())
}
}
// ~~~~~~~~~~~~~~~~~~~~~
// ~ MOD INSERTED HERE ~
// ~~~~~~~~~~~~~~~~~~~~~
mod rare_library {
pub fn internal_mul_by_magic_number(a: felt252) -> felt252 {
a * private_magic_number()
}
fn private_magic_number() -> felt252 {
6
}
}
}
请注意,internal_mul_by_magic_number 和 private_magic_number 这两个函数都没有通过 @self ContractState 访问状态,因此,从 Solidity 的角度来看,它们被认为是 pure 函数。
另请注意,internal_mul_by_magic_number() 被标记为 pub,但 private_magic_number() 没有 pub。这意味着 rare_library 中的函数可以调用 private_magic_number(),但模块外的函数不能。由于 internal_mul_by_magic_number 被标记为 pub,因此可以在 mod 外部调用它。
练习:尝试从 get_balance() 函数调用 private_magic_number()。你应该会收到一个编译错误,确认该函数在其模块外部不可访问。
因为 private_magic_number() 不能被 rare_library 模块之外的任何东西调用,所以我们可以认为它是一个 private 函数。
mod 移动到单独的文件内联 mod 块对于小型模块来说效果很好,但随着模块的增长,它们会使你的合约文件变得混乱。当你需要多个模块时,每个模块都有自己的函数,将所有内容都放在主合约文件中会使查找特定逻辑变得更加困难。
让我们重构我们的代码,将 rare_library 模块移动到一个单独的文件中。这使合约文件专注于合约逻辑,同时隔离了库的模块实现。我们将继续使用上一节中的 internal_demo 项目。
创建一个单独的模块文件
在 src/ 目录中,创建一个名为 rare_lib.cairo 的新文件,并添加以下函数:
pub fn internal_mul_by_magic_number(a: felt252) -> felt252 {
a * private_magic_number()
}
fn private_magic_number() -> felt252 {
6
}
请注意,由于我们位于一个单独的文件中,因此不再需要使用 mod 包装函数;该文件本身充当模块。
更新 src/lib.cairo
现在我们需要更新 src/lib.cairo 以使用我们的新外部模块。进行以下更改:
lib.cairo 的顶部声明模块mod rare_lib;
use crate::rare_lib::{internal_mul_by_magic_number};
get_balance_6x():fn get_balance_6x(self: @ContractState) -> felt252 {
internal_mul_by_magic_number(self.balance.read())
}
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
fn get_balance_6x(self: @TContractState) -> felt252; // 添加这行
}
rare_library mod(因为我们已将其移动到其自己的文件中)。以下是 src/lib.cairo 应如下所示:
mod rare_lib;
/// Interface representing 'HelloContract'.
/// This interface allows modification and retrieval of the contract balance.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: felt252);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> felt252;
fn get_balance_6x(self: @TContractState) -> felt252;
}
/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use crate::rare_lib::{internal_mul_by_magic_number};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
fn get_balance_6x(self: @ContractState) -> felt252 {
internal_mul_by_magic_number(self.balance.read())
}
}
}
更改高亮显示如下:

现在,将以下测试用例添加到测试文件中,以查看 get_balance_6x 即使在单独的文件中,也将余额乘以幻数:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
use internal_demo::IHelloStarknetDispatcher;
use internal_demo::IHelloStarknetDispatcherTrait;
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
contract_address
}
#[test]
fn test_increase_balance() {
let contract_address = deploy_contract("HelloStarknet");
let dispatcher = IHelloStarknetDispatcher { contract_address };
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
dispatcher.increase_balance(42);
let balance_after = dispatcher.get_balance();
assert(balance_after == 42, 'Invalid balance');
}
// 新添加的 //
#[test]
fn test_balance_x6() {
// 部署 HelloStarknet 合约
let contract_address = deploy_contract("HelloStarknet");
// 创建一个 dispatcher 来与合约交互
let dispatcher = IHelloStarknetDispatcher { contract_address };
// 验证初始余额是否为 0
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
// 将余额增加 1
dispatcher.increase_balance(1);
// 调用使用 internal 函数的 get_balance_6x
// 应该返回 1 * 6 = 6 (乘以幻数)
let balance_after_6x = dispatcher.get_balance_6x();
assert(balance_after_6x == 6, 'Invalid balance');
}
运行测试:
scarb test test_balance_6x
你应该看到它通过了,确认我们重构的模块化结构可以正常工作。
在本文中,我们创建了:
get_balance_2x()(可以读取合约状态)internal_mul_by_magic_number()(无法访问状态)private_magic_number()(无法访问状态)当函数不将 self: @ContractState 作为参数时,它们是 pure 的,这意味着它们无法读取或写入合约存储。
注意:我们没有创建可以查看状态的 private 函数。虽然从技术上讲,可以通过将 self: @ContractState 传递给嵌套模块中的函数来实现,但这不是一种常见的模式。在实践中,状态查看函数通常保留为 internal 函数(在单独的 impl 块中),而不是 private 函数(在嵌套模块中),因为 internal 函数已经为大多数用例提供了足够的封装。
impl 块(没有 #[abi(embed_v0)])并添加 #[generate_trait] 属性。这将自动生成一个 trait,使这些函数保留在合约内部。mod。然后在内部 mod 中创建一个 pub fn。此函数可以被外部 mod 访问,但不能被其他任何东西访问。mod 可以放在另一个文件中并导入。只有 pub 函数在外部可见。本文是 Starknet 上的 Cairo 编程 系列教程的一部分
- 原文链接: rareskills.io/post/cairo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!