在学习和使用 Solidity 时,很多人第一次接触 library 的时候,都会遇到这样的报错信息:

TypeError: Name has to refer to a user-defined type

为什么会报这个错?为什么库函数经常被设计为使用 storage 引用?现在我们就通过一个实验来展示 storagememory 的实际区别。


一、报错:Name has to refer to a user-defined type

在 Solidity 中,library 有两种典型的使用方式:

  1. 直接调用库函数
  2. 通过 using ... for 给类型扩展方法

第二种方式是大家常用的,例如:

struct Data { uint value; }

library DataLib {
    function increment(Data storage self) public {
        self.value += 1;
    }
}

contract C {
    using DataLib for Data;

    Data public d;

    function test() public {
        d.increment(); // 自动传入 d
    }
}

但是,如果你写成:

library DataLib {
    function increment(uint storage self) public { 
        self += 1;
    } 
}

编译时就会报错:

TypeError: Name has to refer to a user-defined type

原因在于:通过 using ... for 的库函数,第一个参数必须是用户自定义类型(struct 或 enum),而不能是 uintaddress 等内置类型。


二、为什么库函数使用 storage 引用?

在 Solidity 中,参数有三种数据位置:

  • storage —— 持久化存储在区块链上的数据
  • memory —— 函数调用过程中的临时内存
  • calldata —— 外部调用时的只读数据区

如果库函数参数写成 memory

function increment(Data memory self) public {
    self.value += 1;
}

调用时会复制一份数据,函数里对副本的修改不会影响合约状态。

而如果使用 storage

function increment(Data storage self) public {
    self.value += 1; // 直接修改合约里的状态
}

那么修改会直接作用在合约的存储上,符合大多数“扩展方法”的预期。

好处:

  • 避免复制大型数据结构,节省 gas
  • 语义更接近“对象方法”,调用时就像在修改原对象
  • 确保对合约状态的修改是持久的

三、对比实验:storage vs memory

下面我们通过一个小实验来直观感受差异:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

struct Counter {
    uint count;
}

library CounterLib {
    // 使用 storage,能修改合约状态
    function incStorage(Counter storage self) public {
        self.count += 1;
    }

    // 使用 memory,只会修改副本,不影响状态
    function incMemory(
        Counter memory self
    ) public pure returns (Counter memory) {
        self.count += 1;
        return self;
    }
}

contract C {
    using CounterLib for Counter;

    Counter public counter;

    // 调用 storage 版本
    function callStorage() public {
        counter.incStorage();
    }

    // 调用 memory 版本
    function callMemory() public view {
        Counter memory temp = counter;
        temp = temp.incMemory(); // 只改了副本
        // counter 本身并没有被改变
    }

    function getCount() public view returns (uint) {
        return counter.count;
    }
}

测试过程

使用Foundry编写测试脚本 Counter.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import "../src/Counter.sol";

contract CTest is Test {
    C public counter;

    function setUp() public {
        counter = new C();
    }

    function testIncStorage() public {
        counter.callStorage();
        assertEq(counter.getCount(), 1);
    }

    function testIncMemory() public view {
        counter.callMemory();
        assertEq(counter.getCount(), 0);
    }
}

执行测试脚本:

➜  tutorial git:(main) ✗ forge test --match-path test/Counter.t.sol -vvv
[⠊] Compiling...
[⠔] Compiling 1 files with Solc 0.8.30
[⠒] Solc 0.8.30 finished in 430.02ms
Compiler run successful!

Ran 2 tests for test/Counter.t.sol:CTest
[PASS] testIncMemory() (gas: 13501)
[PASS] testIncStorage() (gas: 32255)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.19ms (1.03ms CPU time)

Ran 1 test suite in 155.29ms (3.19ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

四、总结

  1. 报错原因library 扩展函数的第一个参数必须是用户自定义类型(structenum),否则会报错 “Name has to refer to a user-defined type”
  2. 为什么用 storage
    • storage 允许函数直接修改合约状态
    • 避免大数据复制,节省 gas
    • 语义更自然,像对象方法一样作用于原数据
  3. 实验对比
    • storage 参数:修改持久化状态
    • memory 参数:只修改副本,不会影响状态

所以在设计库函数时,如果你希望修改合约的状态变量,必须使用 storage 引用;如果只是做临时计算,可以用 memorycalldata


孟斯特

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特