本教程分享了一些实用的技巧供您参考。 开发C#NEO智能合约的最大挑战之一是NeoVM支持的语言特性,实际操作中使用的特性比官方文档提供的要多。 还有一些关于存储交互与随机生成的实用技巧。 Enjoy hacking.

类型转换

NeoVM支持的基本类型是位元组数组(Byte []),然后是常用的Boolean,String和BigInteger。 还有其他整数类型,如Int8,Int64,UInt16,long,ulong等,这些可以被隐式转换为BigInteger。 Float类型不受支持。

所以我们只要关注Byte [],Boolean,String和BigInteger之间的转换。 注意:有些转换不是官方定义的,在这种情况下,我会尝试做出最合理的实现。

Byte[] to Boolean

虽然这个看起来是最简单的,但实际上它没有直接转换。官方说明中只提到False等于整数 0。我们假设True等于所有其他值,而空位元组数组等于False。所以我们定义了以下几个函数:

public static bool bybool (byte[] data) => data[0] != 0;

然后可以得到如下结果:

bool b0 = Bytes2Bool(new byte[0]); //False
bool b1 = Bytes2Bool(new byte[1]{0}); //False
bool b2 = Bytes2Bool(new byte[1]{1}); //True
bool b3 = Bytes2Bool(new byte[2]{0,2}); //False
bool b4 = Bytes2Bool(new byte[3]{3,2,5}); //True

Byte[] to String

这个转换直接由Neo.SmartContract.Framework.Helper提供

public static string BytesToByte(byte[] data) => data.AsString();

Byte[] to BigInteger

public static BigInteger BytesToBigInteger(byte[] data) => data.AsBigInteger();

Boolean to Byte[]

这个也需要手工转换。

public static byte[] Bool2Bytes(bool val) => val? (new byte[1]{1}): (new byte[1]{0});

String to Byte[]

public static byte[] StringToByteArray(String str) => str.AsByteArray();

BigInteger to Byte[]

public static byte[]
BigIntegerToByteArray(BigInteger bigInteger) => bigInteger.AsByteArray();

Byte to Byte[]

你可能会认为下面这段代码看起来很好:

public static byte[] Byte2Bytes(byte b) => new byte[1] { b };//WRONG IMPLEMENTATION!!!

它可以通过编译,但在大多数情况下会返回意想不到的值。 这是因为不支持按照变数分配位元组数组。 所以要避免使用这种转换。

操作符和关键字

正如官方文档中提到的,NeoVM支持大多数的c#操作符和关键字。补充说明如下:

Bool: AND, OR NOT

支持操作符「&&」,「||」 「!」

bool b = true;
bool a = false;
Runtime.Notify(!b, b && a, b || a);// 分别代表false, false, true

关键字: 「ref」 「out」

关键字「ref」或「out」是C#语言的特性,用来允许将局部变数传给函数作为引用。Neo智能合约不支持这些关键字。

关键字: 「try-catch」, 「throw」, 「finally」

不支持这几个用于异常处理的关键字

位元组数组:级联和子数组

//Concatenation
public static byte[] JoinByteArrays(byte[] ba1, byte[] ba2) => ba1.Concat(ba2);

//Get Byte arrays subarray
public static byte[] SubBytes(byte[] data, int start, int length) => Helper.Range(data, start, length);

关键字 参数中的「This」

有时你需要定义类型的扩展,从而使逻辑更加简洁直观。 NeoVM支持关键字「This」。 以下示例代码显示了如何使用它。

// Write a static class for the extentions of byte array
public static class ByteArrayExts{
// Return The subarray
public static byte[] Sub(this byte[] bytes, int start, int len){
return Helper.Range(bytes, start, len);
}
// Return the reversed bytearray
public static byte[] Reverse(this byte[] bytes){
byte[] ret = new byte[0];
for(int i = bytes.Length -1 ; i>=0 ; i--){
ret = ret.Concat(bytes.Sub(i,1));
}
return ret;
}
}

使用上面的方法:

byte[] ba0 = {1,31,41,111};
byte[] ba1 = {12,6,254,0,231};
//Calls the Reverse and Sub functions with only one line.
Runtime.Notify(ba0, ba1, ba0.Reverse(), ba1.Sub(1,2));
//Call the extension functions multiple times in a row.
Runtime.Notify(ba1.Sub(0,3).Reverse());

位元组数组:修改值

NeoVM不支持可变位元组操作。 所以我们需要拆分子数组,修改其中的一部分值,然后再将它们连接起来。 应将下面这个方法放入上面的ByteArrayExts类中。

public static class ByteArrayExts{
//... previous functions ...

public static byte[] Modify(this byte[] bytes, int start, byte[] newBytes){
byte[] part1 = bytes.Sub(0,start);
int endIndex = newBytes.Length + start;
if(endIndex < bytes.Length){
byte[] part2 = bytes.Sub(endIndex, bytes.Length-endIndex);
return part1.Concat(newBytes).Concat(part2);
}
else{
return part1.Concat(newBytes);
}
}
}

使用:

byte[] orig = new byte[5]{1,2,3,4,5};
byte[] newValue = new byte[2]{6,7};

//Replace the 3rd and 4th elements of orig byte array.
byte[] ret = orig.Modify(2, newValue);//return {1,2,6,7,5};

存储

Storage / StorageMap类是与智能合约的链上持久化信息进行交互的唯一方式。 基本的CRUD操作是:

//Create and Update: 1GAS/KB
Storage.Put(Storage.CurrentContext, key, value);

//Read: 0.1GAS/time
Storage.Get(Storage.CurrentContext, key);

//Delete: 0.1GAS/time
Storage.Delete(Storage.CurrentContext, key);

在使用上面这几个方法时,有一些技巧:

1.在调用Storage.Put()之前检查值是否保持不变。 如果不改变,这将节省0.9GAS。

2.在调用Storage.Put()之前,检查新值是否为空。 如果为空,请改用Storage.Delete()。 这也将节省0.9GAS。

byte[] orig = Storage.Get(Storage.CurrentContext, key);
if (orig == value) return;//Dont invoke Put if value is unchanged.

if (value.Length == 0){//Use Delete rather than Put if the new value is empty.
Storage.Delete(Storage.CurrentContext, key);
}
else{
Storage.Put(Storage.CurrentContext, key, value);
}

3. 设计数据结构时预估长度接近但小于n KB。因为方法写2位元组和写900位元组的开销是一样的。如有必要,你甚至可以组合一些项。

BigInteger[] userIDs = //....Every ID takes constantly 32 Bytes.
int i = 0;
BigInteger batch = 0;
while( i< userIDs.Length){
byte[] record = new byte[0];
for(int j = 0; j< 31;j++){//31x32 = 992 Bytes.
int index = i + j;
if( index == userIDs.Length ) return;
else{
record=record.Concat(userIDs[index].AsByteArray());
}
}
//This cost only 1GAS rather than 31GAS.
Storage.Put(Storage.CurrentContext, batch.AsByteArray(), record);
batch = batch + 1;
++i;
}

随机性

生成随机值对于智能合约来说是一项挑战。

首先,种子必须是区块链相关的确定性值。 否则,记账员就不能同意。 大多数Dapps会选择blockhash作为种子。但是使用这种方法的话,不同的用户在同一个区块中调用相同的SC方法会返回相同的结果。在Fabio Cardoso的文章中,引入了一种新的演算法来同时使用blockhash和transactionID作为种子。

对于一些高度敏感的Dapps,专业用户可能会争辩说,记账员可以通过重新排序交易来干预blockhashes。 在这种情况下,generalkim00和maxpown3r提供了非对称熵的演算法。 这个过程有点复杂,所以要想学习的话,可以点击这个链接阅读他们这个博彩的例子的智能合约源代码。

总结

感谢阅读本教程。如果我们在开发智能合约时发现更多技巧的话,我会继续在这里更新它。感谢dprat0821在讨论中给予的帮助。感谢Fabio, generalkim00和maxpown3r的精彩想法。

我的团队正在开发一款将人们内心深处的话语刻在NEO区块链上的游戏。谢谢你的意见和建议。

NEO 捐赠

地址: AKJEavjHZ3v96kxh7nWKpt4nVCj7VtirCg

原文链接:medium.com/@gongxiaojin

翻译:包子


NEOFANS:neofans.org

NEOFANS 微博:weibo.com/neofanscommun

NEOFANS telegram群:t.me/NEOfansCN


推荐阅读:
相关文章