Cairo 没有像 Solidity 那样的 “internal”(内部)和 “pure”(纯)修饰符(或者说没有任何其他类似的修饰符)。
回想一下,使用 #[abi(embed_v0)] 标记 impl 块会告诉 Cairo 将其函数包含在合约的 ABI(应用程序二进制接口)中,从而使它们可以从合约外部调用。这个 impl 块中的函数必须在它所实现的 trait 中定义(类似于 Solidity 中的接口);这确保了外部调用者确切地知道哪些函数是可用的以及如何调用它们。
但是那些不应该被外部调用的函数呢?与 Solidity 一样,Cairo 也能够限制函数能做什么、不能做什么,以及它们的函数可见性。
在本文中,我们将展示如何在 Cairo 中实现与内部(internal)、私有(private)和纯(pure)函数等效的功能。
内部函数演示
我们的第一个演示是一个内部视图(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> {
// ... existing functions (increase_balance, get_balance) ...
// NEWLY ADDED FUNCTION
// Note: This function will throw error
fn get_balance_2x(self: @ContractState) -> felt252 {
self.balance.read() * 2
}
}
}
我们会得到一个编译错误,因为 get_balance_2x 不是 IHelloStarknet 的一部分。

如前所述,在 Cairo 中实现一个 trait 时,impl 块只能包含该 trait 中定义的函数。一个合约可以有多个 impl 块,不属于该 trait 的函数必须定义在单独的 impl 块中。这与 Solidity 不同,在 Solidity 中,合约可以在其实现的接口之外自由添加函数。
然而,我们特别不想将 get_balance_2x 包含在 IHelloStarknet trait 中,因为那样会使该函数成为公开的(public)。
解决因将 get_balance_2x 包含在 HelloStarknetImpl 块中(而未添加到 trait 中)导致的编译错误的解决方案是:
- 将
get_balance_2x放入一个单独的impl块中。 - 让该
impl块使用一个单独的 trait。
在 HelloStarknetImpl 实现之后,将以下代码添加到 HelloStarknet 合约模块中:
// NEWLY ADDED //
#[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');
}
测试将无法编译,因为该函数在公开层面上是不可见的:

为了测试内部函数是否按预期工作,我们将添加另一个公开函数 extern_wrap_get_balance_2x,然后如下所示通过 self 变量访问我们的内部函数。
别忘了我们还需要将此函数添加到接口中(如下方红框所示),因为我们希望它能在合约外部被访问:

extern_wrap_balance_2x 函数(蓝框)调用了返回两倍当前余额的内部函数(绿框)。以下是完整代码:
#[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() {
// Deploy the HelloStarknet contract
// Note: deploy_contract is a helper function from the test setup
let contract_address = deploy_contract("HelloStarknet");
// Create a dispatcher to interact with the contract
let dispatcher = IHelloStarknetDispatcher { contract_address };
// check initial balance is 0
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
// Increase balance by 1
dispatcher.increase_balance(1);
// Call the wrapper function that uses our internal function
// Should return 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 中创建内部函数的方法是,将它们定义在一个没有 #[abi(embed_v0)] 的独立 impl 块中,并使用 #[generate_trait] 自动生成该 impl 块所实现的 trait。这使得函数可以在合约内被调用,但对外部调用者隐藏。
Cairo 中的私有视图和纯函数
在 Solidity 中,“私有(private)”函数和“内部(internal)”函数的区别在于,子合约可以看到“内部”函数,但“私有”函数只能被包含该函数的合约看到。
Cairo 没有继承机制,因此在 Cairo 中提到“私有”函数时,我们必须非常严谨。
然而,一个自然而然的问题出现了:是否有可能将函数的可见性“模块化”?例如,在 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 中的私有函数那样。
使用嵌套模块实现私有函数
到目前为止,我们只把 mod(“模块”)看作是合约函数的“容器”。然而,Cairo 允许我们使用嵌套模块来进行进一步的模块化。我们可以使用这种模式来实现类似于 Solidity 中私有函数的功能。
下面是生成新 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 的角度来看,它们被认为是纯函数。
还要注意,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 模块外部的任何东西调用,所以我们可以把它看作是一个私有函数。
将 mod 移动到单独的文件中
内联(Inline)的 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())
}
- 将该函数添加到接口 trait 中(否则将无法编译):
#[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; // ADD THIS LINE
}
- 删除上一节中的内联
rare_library模块(因为我们已经将其移到了自己的文件中)。
以下是 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');
}
// NEWLY ADDED //
#[test]
fn test_balance_x6() {
// Deploy the HelloStarknet contract
let contract_address = deploy_contract("HelloStarknet");
// Create a dispatcher to interact with the contract
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Verify initial balance is 0
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
// Increase balance by 1
dispatcher.increase_balance(1);
// Call get_balance_6x which uses the internal function
// Should return 1 * 6 = 6 (multiplied by the magic number)
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 作为参数时,它们就是纯函数,这意味着它们不能读写合约的存储。
**注意:**我们没有创建可以读取状态的私有函数。虽然在技术上可以通过将 self: @ContractState 传递给嵌套模块中的函数来实现,但这并不是一种常见的模式。在实践中,读取状态的函数通常作为内部函数(在单独的 impl 块中)而不是私有函数(在嵌套模块中)来保留,因为对于大多数用例来说,内部函数已经提供了足够的封装。
总结
- 要创建内部函数,请定义一个单独的
impl块(不带#[abi(embed_v0)])并添加#[generate_trait]属性。这会自动生成一个 trait,从而使这些函数保持为合约的内部函数。 - 要创建一个纯函数(无法访问状态的函数),请在合约内声明一个
mod。然后在内部的mod中创建一个pub fn。该函数可被外部的mod访问,但其他任何地方都不能访问。 - 一个
mod可以放在另一个文件中并被导入。只有pub函数在外部才是可见的。
本文是 Starknet 上的 Cairo 编程 系列教程的一部分