Cairo 中的函数可见性

本文介绍了如何在 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 函数演示

我们的第一个演示是一个 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 的一部分。

Screenshot 2025-10-14 at 12.14.23.png

如前所述,在 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 块以红色高亮显示:

Cairo code with an internal function highlighted

  • 名称 InternalFunction 是完全任意的;它可以是对于合约有意义的任何名称。
  • 由于每个 impl 块都需要一个关联的 trait,因此我们将其命名为 IInternal(也是任意的)。
  • 我们不需要显式地为 internal 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');
}

测试将无法编译,因为该函数不是公开可见的:

Cairo compilation issue due to visibility mismatch

为了测试 internal 函数是否按预期工作,我们将添加另一个函数 extern_wrap_get_balance_2x,它将是公共的,然后通过 self 变量访问我们的 internal 函数,如下所示。

不要忘记我们还需要将此函数添加到接口(如下面的红色框中所示),因为我们希望它可以从合约外部访问:

Code to make a Cairo function publicly visible

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 来创建的。这使函数可以在你的合约中调用,但对外部调用者隐藏。

Cairo 中的 Private view 和 pure 函数

在 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(),因为它继承了 AB 看不到 private_magic_number()

但是,B 中的 external_fun() 在调用 internal_mul_by_magic_number() 时“在幕后” 使用 private_magic_number()

让我们在 Cairo 中创建一个相同的构造,以展示一个函数如何像 Solidity 中的 private 函数一样对代码的其他部分不可用。

使用嵌套模块实现 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_numberprivate_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;
  • 导入我们要使用的“internal”函数:
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;  // 添加这行
   }
  • 删除我们在上一节中拥有的内联 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())
        }
    }
}

更改高亮显示如下:

Importing a module from another file in Cairo

现在,将以下测试用例添加到测试文件中,以查看 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

你应该看到它通过了,确认我们重构的模块化结构可以正常工作。

总结

在本文中,我们创建了:

  • Internal view 函数:get_balance_2x()(可以读取合约状态)
  • Internal pure 函数:internal_mul_by_magic_number()(无法访问状态)
  • Private pure 函数:private_magic_number()(无法访问状态)

当函数不将 self: @ContractState 作为参数时,它们是 pure 的,这意味着它们无法读取或写入合约存储。

注意:我们没有创建可以查看状态的 private 函数。虽然从技术上讲,可以通过将 self: @ContractState 传递给嵌套模块中的函数来实现,但这不是一种常见的模式。在实践中,状态查看函数通常保留为 internal 函数(在单独的 impl 块中),而不是 private 函数(在嵌套模块中),因为 internal 函数已经为大多数用例提供了足够的封装。

总结

  • 要创建 internal 函数,请定义一个单独的 impl 块(没有 #[abi(embed_v0)])并添加 #[generate_trait] 属性。这将自动生成一个 trait,使这些函数保留在合约内部。
  • 要创建一个 pure 函数(无法访问状态的函数),请在合约中声明一个 mod。然后在内部 mod 中创建一个 pub fn。此函数可以被外部 mod 访问,但不能被其他任何东西访问。
  • mod 可以放在另一个文件中并导入。只有 pub 函数在外部可见。

本文是 Starknet 上的 Cairo 编程 系列教程的一部分

  • 原文链接: rareskills.io/post/cairo...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/