solidity自学手册
官⽹:http://solidity.readthedocs.io/en/develop/
Solidity是⼀个⾯向对象的⾼级语⾔,其语法是类JavaScript,是运⾏在以太坊虚拟机中的代码,它是静态类型的编程语⾔。
版本申明
pragma solidity ^0.4.0;
^0.4.0代表solidity编译器的版本号,第⼆位的4是主版本,第三位的4是
⼩版本,^符号表示版本向上兼容,即 0.4.4 ~ 0.5.0(不包含0.5.0) 之间的solidity均可编译该合约代码,⼀般修
复bug时会更新⼩版本,⼤调整时修改主版本
全局引⼊
import “filename”
⾃定义命名空间引⼊
import * as symbolName from “filename”
全局引入案例
hello.sol 先进行部署
pragma solidity ^0.4.21; contract Test{ string str1; function setValue (string para) public { str1 = para; } function getValue() constant public returns (string){ return str1; } }
importTest.sol 再进行部署 参数填写之前合约的地址,”str” 会修改第一个合约的数值
pragma solidity ^0.4.20;
import "./hello.sol";
contract ImportTest {
function setString (Test test, string str) public {
test.setValue(str);
}
}
合约的结构(基本组成元素)
每个合约中可包含状态变量(State Variables),函数(Functions),函数修饰符(Function Modifiers),事件
(Events),结构类型(Structs Types)和枚举类型(Enum Types)
合约类似于⾯向对象语⾔中的 类 ,且 ⽀持继承
状态变量(State Variables)
变量值会永久存储在合约的存储空间
pragma solidity ^0.4.0; // simple store example contract simpleStorage{ uint valueStore; //state variable }
函数(Functions)
智能合约中的⼀个可执⾏单元
pragma solidity ^0.4.0; contract simpleMath{ //Simple add function,try a divide action? function add(uint x, uint y) returns (uint z){ z = x + y; } }
上述代码实现了⼀个简单的加法函数。合约函数的调⽤分为 内部调⽤internal 和 外部调⽤external ,后续函
数章节会详细介绍。
函数修饰符(Function Modifiers)
pragma solidity ^0.4.22; contract Purchase { address public seller; modifier onlySeller() { // Modifier require( msg.sender == seller, "Only seller can call this." ); _; } function abort() public onlySeller { // Modifier usage // ... } }
事件(Events)
事件是以太坊虚拟机(EVM)⽇志基础设施提供的⼀个便利接⼝,⽤于⽇志输出,便于跟踪调试
pragma solidity ^0.4.21; contract SimpleAuction { event HighestBidIncreased(address bidder, uint amount); // Event function bid() public payable { // ... emit HighestBidIncreased(msg.sender, msg.value); // Triggering event } }
———————————————————————————————————————-
002.数据类型
值传递与引用传递
值传递 memory(Value Type) 引用传递 storage(Refernce Type) 将改变变量的值 类似于& 红色部分如果改为memory(默认)将不改变_name的值 如果改为 storage 引用传递 函数类型应改为private 在函数内部进行调取
pragma solidity ^0.4.20;
contract Student{
string _name = "lily";
uint _num;
function execute() public{
changeName(_name);
}
function changeName(string storage name) private{
_num = 10;
bytes(name)[0] = "L";
}
function getName() constant public returns (string) {
return _name;
}
function getNum() constant public returns (uint){
return _num;
}
}
//memory(Value Type) storage(Refernce Type)
003.布尔类型
pragma solidity ^0.4.20; contract BoolTest { uint v1 = 10; uint v2 = 20; string s1 = "hello"; string s2 = "world"; boo flag1 = true; bool flag2 = false; function f1() constant public returns (bool) { return !flag1; } function f2() constant public returns (bool) { return flag1 && flag2; } function f3() constant public returns (bool) { return flag1 || flag2; } function f4() constant public returns (bool) { return v1 == v2; } function f5() constant public returns (bool) { return v1 != v2; } }
004.整型
pragma solidity ^0.4.21; contract IntegerTest { /* //&,|,^(异或),~(位取反) 10 uint8 1010 4 uint8 0100 & 0000 0 | 1110 14 ^ 1110 14 ~ 3 0000,0011 1100 12 1111,1100 1111,1111 - 3 2^8 -1 -3 = 256 -4 = 252 */ uint8 a = 10; uint8 b = 4; uint8 c = 3; function f1() constant public returns (bool) { return a & b == 0; } function f2() constant public returns (bool) { return a | b == 14; } function f3() constant public returns (bool) { return a ^ b == 14; } function f4() constant public returns (bool) { return (~c) == 252; } function f5() constant public returns (uint8){ return ~c; } }
地址(Address)
以太坊地址的⻓度,⼤⼩ 20个字节 , 160位 ,所以可以⽤⼀个 uint160 编码。地址是所有合约的基础,所有
的合约都会继承地址对象,也可以随时将⼀个地址串,得到对应的代码进⾏调⽤。
⽀持的运算符:
描述 符号
⽐较运算符 <=,<,==,!=,>=,>
地址类型的成员:
属性: balance
⽅法: send() , transfer() , call() , delegatecall() , callcode()
地址与整型转换
pragma solidity ^0.4.20; contract AddressTest { address _add1 = 0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db; address _add2 = 0x583031d1113ad414f02576bd6afabfb302140225; //加public会有一个匿名函数方便展示 uint160 public _num = 0; address public _add; function address2uint160() public returns(uint160){ _num = uint160(_add1); return _num; } function uint160ToAddress() public returns(address){ _add = address(_num); return _add; } function comAddress() constant public returns(bool){ return _add1 == _add2; } function isGreater() constant public returns(bool){ return _add1 > _add2; } }
balance余额
pragma solidity ^0.4.20; contract addressBalance { function getBalance(address addr) constant public returns (uint){ return addr.balance; } }
sender消息发起人
pragma solidity ^0.4.20; contract senderTest { address public _owner; //存储合约创建者地址 // 构造函数 function senderTest() public { _owner = msg.sender; // 是合约创建者地址 只执行一次 } function getOwnerBalance() public returns (uint256){ return msg.sender.balance; // 每一个sender发送者的钱包地址 地址会变动 } function getInvoker() constant public returns (address){ return msg.sender; // 每一个sender发送者的余额 地址会变动 } }
this获取合约信息及地址
表示智能合约的地址的变量: this指针
如果只是想返回当前合约账户的余额,可以使⽤ this 指针, this 表示合约⾃身的地址
对于合约来说,地址代表的就是合约本身,合约对象默认继承⾃地址对象,所以内部有地址的属性。
注:this和msg.sender不⼀样, this代表的是合约本身的地址,msg.sendr返回的是请求合约的账户的地址
pragma solidity ^0.4.20; contract addressThis { uint _money; // 构造函数 function addressThis() payable public { //payable 表示这个合约可以接受钱了 _money = msg.value; //合约部署时value字段 给的金额 } function getThis() constant public returns (address){ return this; //返回合约本身 } function getBalance () constant public returns (uint256){ return this.balance; //合约余额 } function getMoney() constant public returns(uint){ return _money; } function getMsgSender() constant public returns (address){ return msg.sender; //请求发送者地址 变化的 } }
转账transfer介绍
pragma solidity ^0.4.20; //在测试网测试通过合约进行转账 //注意value字段在部署合约的时候不要写 当合约部署完成后 调取转账函数时再填写value字段 //acc2 : 8.9 0x794C3fF2068455dbeb5D5D039347d221d10C905f //Alice : 12.9 0xa446742e4845CFbA8EC7d93226eE6ceB6C7C91ab contract TransferTest { address AliceAddress = 0xa446742e4845CFbA8EC7d93226eE6ceB6C7C91ab; function transfer() payable { //为了避免误操作 转账函数必须含有payable关键字才可以 AliceAddress.transfer(msg.value); } }
转账send()函数
这两个⽅法都是⽤来发送货币的,使⽤的参数也都⼀致。不同之处在于:合约运⾏出错或由于gas耗尽⽽停
⽌时, transfer() ⽅法会抛出异常,⽽ send() ⽅法相对于 transfer() ⽽⾔更底层,它不抛异常,⽽是会
返回false。
注:send() 执⾏有⼀些⻛险:如果调⽤栈的深度超过1024或gas耗光,交易都会失败。因此,为了保证安
全,必须检查send的返回值,如果交易失败,会回退以太币。如果⽤transfer会更好。
pragma solidity ^0.4.20; //acc8 : 6.9 0xa7eEEAC067530608Ed24E76c47A57d1b35702cce //Alice : 11.9 0xa446742e4845CFbA8EC7d93226eE6ceB6C7C91ab //注意value字段在部署合约的时候不要写 当合约部署完成后 调取转账函数时再填写value字段 contract TranferTest { address AliceAddress = 0xa446742e4845CFbA8EC7d93226eE6ceB6C7C91ab; bool res; function transfer() payable public returns (bool){ //send()方法转账更底层与transfer用法一样 但是send方法使用处理不当会有风险 res = AliceAddress.send(msg.value); return res; } }
006.枚举类型
枚举类型是在Solidity中的⼀种⽤户⾃定义类型。他可以显示的转换与整数进⾏转换,但不能进⾏隐式转
换。显示的转换会在运⾏时检查数值范围,如果不匹配,将会引起异常。枚举类型应⾄少有⼀名成员,枚举
元素默认为uint8,当元素数量⾜够多时,会⾃动变为uint16,第⼀个元素默认为0,使⽤超出范围的数值时
会报错。
pragma solidity ^0.4.0; contract enumTest { enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill } //uint8 顺序 0 1 2 3 ActionChoices _choice; ActionChoices defaultChoice = ActionChoices.GoStraight; //设置 0 1 2 3 function setGoStraight(ActionChoices choice) public { _choice = choice; } //读取 0 1 2 3 数字 function getChoice() constant public returns (ActionChoices) { return _choice; } //返回2 function getDefaultChoice() constant public returns (uint) { return uint(defaultChoice); } //输入0返回true function isGoLeft(ActionChoices choice) constant public returns (bool){ if (choice == ActionChoices.GoLeft){ return true; } return false; } }
007.函数类型 内部外部类型函数
pragma solidity ^0.4.5; contract FuntionTest{ uint public v1; uint public v2; /* uint public v1 就类似于下面这个函数 在部署时可以查看变量数值 function v1() constant returns (uint) { return v1; } */ // 只能在合约内部调取 外部合约无法调用 function internalFunc() internal{ v1 = 10; } function externalFunc() external returns (uint res){ v2 = 20; return v2; } function resetV2() public { v2 = 0; } function callFunc() { //直接使用内部的方式调用 internalFunc(); //<--- 合约内部直接调用,正确 //不能在内部调用一个外部函数,会报编译错误。 //Error: Undeclared identifier. //externalFunc(); //<--- 外部合约可以调用 //不能通过`external`的方式调用一个`internal` //Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest //this.internalFunc(); //<--- this相当于外部调用 //使用`this`以`external`的方式调用一个外部函数 this.externalFunc(); } } // 继承 FuntionTest 所有方法 可以用internal 不可以用external contract Son is FuntionTest { function callInternalFunc() public{ internalFunc(); //externalFunc(); 无法调取外部函数 } } contract FunctionTest1{ uint public v3; function externalCall(FuntionTest ft){ //FuntionTest ft 参数 输入第一个合约部署的合约地址 //调用另一个合约的外部函数 v3 = ft.externalFunc(); //得到20 将改变v3结果 //不能调用另一个合约的内部函数 //Error: Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest //ft.internalFunc(); } function resetV3() public { v3 = 0; } }
008.数组
1. 定⻓数组bytes1 … bytes32
字节不可修改
⻓度不可修改
⽀持 length (只读)
⽀持下标索引
pragma solidity ^0.4.0; contract C { bytes10 public b = "helloworld"; uint public len; function f() public { len = b.length; //b.length = 10; //ERROR,⻓度为只读,不可修改 } bytes1 public c = b[0]; //b = "HELLO";ERROR,定义之后不可修改 }
byte[n]
引⽤类型
⽀持下标
⻓度可不变
可以修改
pragma solidity ^0.4.4; contract C { bytes9 bb = 0xabcde; byte[9] aa = [byte(0x6c),0x69,0x79,0x75,0x65,0x63,0x68,0x75,0x6e]; byte[8] cc = [byte("a"), "b"]; uint len = aa.length; function test() public{ aa[0] = 0xa1; //bb[0] = 0xa1; } }
不定⻓数组
内容和⻓度均可修改,包括以下三种: 类型[⻓度] 、 bytes 、 string
第⼀种:类型[⻓度]
动态数组
可以修改
可以改变⻓度(storage)
⽀持 length 、 push ⽅法
可使⽤new关键字创建⼀个memory的数组。与stroage数组不同的是,你不能通过.length的⻓度来修改数组
⼤⼩属性。我们来看看下⾯的例⼦:
pragma solidity ^0.4.0; contract C { function f() { //创建⼀个memory的数组 uint[] memory a = new uint[](7); //不能修改⻓度 //Error: Expression has to be an lvalue. //a.length = 100; for (uint i = 0; i< a.length; i++){ a[i] = i; } //a.push(10); } //storage uint[] public b; function g(){ b = new uint[](7); //可以修改storage的数组 b.length = 10; b[9] = 100; b.push(101); } }
直接创建
* 内容可变
* ⻓度不可变
* ⽀持length⽅法,不⽀持push
pragma solidity ^0.4.5; contract test { uint [10] value = [1,2,3,4,5]; uint public sum; function getSum(){ sum = 0; for (uint i = 0; i < value.length; i++){ sum += value[i]; } } function changeValue(){ value[0] = 2; } }
第⼆种:bytes 动态字节数组
引⽤类型
⽀持 下标索引
⽀持 length 、 push ⽅法
可以修改
pragma solidity ^0.4.4; contract C { bytes public _name = new bytes(1); bytes public _name2; function setLength(uint length) { _name.length = length; } function getLength() constant returns (uint) { return _name.length; } function setName(bytes name){ _name = name; } function changeName(bytes1 name){ _name[0] = name; } function setInside(){ _name = "helloWorld"; _name2 = "HELLOWORLD2"; } }
第三种:string
动态尺⼨的UTF-8编码字符串,是特殊的可变字节数组
引⽤类型
不⽀持下标索引
不⽀持 length 、 push ⽅法
可以修改(需通过bytes转换)
注:bytes和string可以⾃由转换
string转换成bytes
pragma solidity ^0.4.4; contract C { string public _name = "lily"; function nameBytes() constant returns (bytes) { return bytes(_name); } function nameLength() constant returns (uint) { return bytes(_name).length; } function changeName() public { bytes(_name)[0] = "L"; //s2[0] = "H"; //ERROR,不⽀持下标索引 } function changeLength() { bytes(_name).length = 15; bytes(_name)[14] = "x"; } }
pragma solidity ^0.4.20; contract Test { bytes10 b10 = 0x68656c6c6f776f726c64; //helloworld 文本转为16进制 bytes public bs10 = new bytes(b10.length); //1. 固定字节数组转动态字节数组 function fixedBytesToBytes() public{ for (uint i = 0; i< b10.length; i++){ bs10[i] = b10[i]; } } //2、字符串转为bytes string greeting = "helloworld"; bytes public b1; function StringToBytes() public { b1 = bytes(greeting); } //3.string转动态字节数组 string public str3; function BytesToString() public { str3 = string(bs10); } }
009.结构体
pragma solidity ^0.4.5; contract Test { struct Student { string name; uint age; uint score; string sex; } Student public stu1 = Student("lily",18,90,"girl"); Student public stu2 = Student({name:"jim",age:20,score:80,sex:"boy"}); Student[] public Students; function assign() public { Students.push(stu1); Students.push(stu2); stu1.name = "Lily"; } }
010.映射/字典 (mapping)
键的类型允许除映射外的所有类型,如数组,合约,枚举,结构体。值的类型⽆限制。
creates a namespace in which all possible keys exist, and values are initialized to 0/false.
所以⽆法判断⼀个mapping中是否包含某个key,因为它认为每⼀个都存在,只不过是0或false
映射可以被视作为⼀个哈希表,其中所有可能的键已被虚拟化的创建,被映射到⼀个默认值(⼆进制表示的
零)。在映射表中,我们并不存储键的数据,仅仅存储它的 keccak256 哈希值,⽤来查找值时使⽤。
映射类型,仅能⽤来定义状态变量,或者是在内部函数中作为storage类型的引⽤
pragma solidity ^0.4.20; contract test { // id -> name mapping(uint => string) id_names; constructor() public { id_names[0x001] = "lily"; id_names[0x002] = "Jim"; id_names[0x002] = "mike"; } function getNameById(uint id) constant public returns (string){ string storage name = id_names[id]; return name; } }
11.⾃动推导类型
为了⽅便,并不总是需要明确指定⼀个变量的类型,编译器会通过第⼀个向这个对象赋予的值的类型来进⾏
推断
uint24 x = 0x123; var y = x;
函数的参数,包括返回参数,不可以使⽤var这种不指定类型的⽅式。
需要特别注意的是,由于类型推断是根据第⼀个变量进⾏的赋值。所以下⾯的代码将是⼀个⽆限循环,,因
为⼀个uint8的i的将⼩于2000。
for (var i = 0; i < 2000; i++) { //⽆限循环 }
演示
pragma solidity ^0.4.4; contract Test{ function a() returns (uint){ uint count = 0; for (var i = 0; i < 257; i++) { count++; if(count >= 258){ break; } } return count; } }
12.全局函数
将如下代码使用remix在metamask进行部署,并且与合约进行交互调取 tt()方法进行查看 如果遇到GAS不足错误提示 增大GAS费用 (增大“燃料限制”数字)
pragma solidity ^0.4.21; contract Test { bytes32 public _blockhash; address public coinbase; uint public difficulty; uint public gaslimit; uint public blockNum; uint public timestamp; bytes public calldata; uint public gas; address public sender; bytes4 public sig; uint public msgValue; uint public _now; uint public gasPrice; address public txOrigin; function tt () payable public { //给定区块号的哈希值,只支持最近256个区块,且不包含当前区块 _blockhash = blockhash(block.number - 1); coinbase = block.coinbase ;//当前块矿工的地址。 difficulty = block.difficulty;//当前块的难度。 gaslimit = block.gaslimit;// (uint)当前块的gaslimit。 blockNum = block.number;// (uint)当前区块的块号。 timestamp = block.timestamp;// (uint)当前块的时间戳。 calldata = msg.data;// (bytes)完整的调用数据(calldata)。 gas = gasleft();// (uint)当前还剩的gas。 sender = msg.sender; // (address)当前调用发起人的地址。 sig = msg.sig;// (bytes4)调用数据的前四个字节(函数标识符)。 msgValue = msg.value;// (uint)这个消息所附带的货币量,单位为wei。 _now = now;// (uint)当前块的时间戳,等同于block.timestamp gasPrice = tx.gasprice;// (uint) 交易的gas价格。 txOrigin = tx.origin;// (address)交易的发送者(完整的调用链) } }
13.货币单位与时间单位介绍
pragma solidity ^0.4.0; contract EthUnit{ uint a = 1 ether; uint b = 10 ** 18 wei; uint c = 1000 finney; uint d = 1000000 szabo; function f1() constant public returns (bool){ return a == b; } function f2() constant public returns (bool){ return a == c; } function f3() constant public returns (bool){ return a == d; } function f4() pure public returns (bool){ return 1 ether == 100 wei; } } contract TimeUnit{ function f1() pure public returns (bool) { return 1 == 1 seconds; } function f2() pure public returns (bool) { return 1 minutes == 60 seconds; } function f3() pure public returns (bool) { return 1 hours == 60 minutes; } function f4() pure public returns (bool) { return 1 days == 24 hours; } function f5() pure public returns (bool) { return 1 weeks == 7 days; } function f6() pure public returns (bool) { return 1 years == 365 days; } }
constant、view、pure介绍
pragma solidity ^0.4.20; contract Test { //一、constant介绍 uint public v1 = 10; uint constant v2 = 10; string str1 = "hello!"; string constant str2 = "test!"; function f1() public { v1 = 20; //v2 = 30; //constant修饰的值类型无法被修改 str1 = "Hello!"; // str2 = "Test!"; } struct Person { string name; uint age; } //错误的 -> Person constant p1; //constant仅可以修饰值类型,无法修饰引用类型(string除外) function f2() constant public{ v1 = 20; //constant 修饰的函数内,如果修改了状态变量,那么状态变量的值是无法改变的,小心!! } //二、view介绍 // 1. view只可以修饰函数 // 2. 它表明该函数内尽可以对storage类型的变量进行读取,无法修改 //三、pure介绍 //pure // 1. pure只可以修饰函数 // 2. 它表明该函数内,无法读写状态变量 function f3() pure public returns(uint){ //return v1; 读写都错误 } }
15.错误处理
传统⽅法:采⽤ throw 和 if … throw 模式(已过时)
例如合约中有⼀些功能,只能被授权为 拥有者 的地址才能调⽤。
新⽅法:
新函数 require() , assert() , revert() 提供了同样功能,⽽且上下⽂更加⼲净。
下⾯的代码:
if(msg.sender != owner) { throw; }
等价于:
if(msg.sender != owner) { revert(); }
assert(msg.sender == owner);
require(msg.sender == owner);
注意在 assert() 和 require() 例⼦中的条件声明,是 if 例⼦中条件块取反,也就是⽤ == 代替了 != 。
pragma solidity ^0.4.21; contract HasAnOwner { address public owner; uint public a ; constructor() public { owner = msg.sender; } function useSuperPowers() public { require(msg.sender == owner); /* 等同于 如果发送者不是创建合约者会抛出异常 if (msg.sender != owner){ throw; } */ a = 10; // do something only the owner should be allowed to do } }
16.delete介绍
delete操作符可以⽤于任何变量,将其设置成默认值
如果对动态数组使⽤delete,则删除所有元素,其⻓度变为0
如果对静态数组使⽤delete,则重置所有索引的值
如果对map类型使⽤delete,什么都不会发⽣
指定键删除:但如果对map类型中的⼀个键使⽤delete,则会删除与该键相关的值
pragma solidity ^0.4.21; contract deleteTest { string public str1 = "hello"; //delete操作符可以用于任何变量(mapping除外),将其设置成默认值 function delStr() public{ delete str1; } function setStr() public { str1 = "HELLO"; } //静态数组,动态数组 uint[10] public staticArray = [4,2,4,5,6,7,8]; uint[] public dynamicArray = new uint[](10); function intDynamicArray () public { for (uint i = 0; i< 10; i++) { //dynamicArray[i] = i; dynamicArray.push(i); } } //如果对静态数组使用delete,则重置所有索引的值 function delStaticArray() public { delete staticArray; } //如果对动态数组使用delete,则删除所有元素,其长度变为0 function delDynamicArray() public { delete dynamicArray; } function getArrayLength() view public returns (uint, uint){ return (staticArray.length, dynamicArray.length); } mapping(uint => bool) public map1; function initMap() public { map1[1] = true; map1[2] = true; map1[3] = false; //delete map1; } function deleMapByKey(uint key) public { delete map1[key]; } //delete map1; struct Person { string name; mapping(string => uint) nameScore; } Person public p1; function initP1() public { p1.name = "duke"; p1.nameScore["duke"] = 80; } function returnP1() view public returns (string, uint) { return (p1.name, p1.nameScore["duke"]); } function deleteP1() public { delete p1; } }
17.modifier修饰器介绍
修改器(Modifiers)可以⽤来轻易的改变⼀个函数的⾏为。⽐如⽤于在函数执⾏前检查某种前置条件。修改器
是⼀种合约属性,可被继承,同时还可被派⽣的合约重写(override)。下⾯我们来看⼀段示例代码:
本代码在运行useSuperPowers()函数时先运行ownerOnly() 修饰器条件校验 校验成功后 _;代码回换为 a = 10; 要执行的代码 直接执行 否则将失败
pragma solidity ^0.4.21; contract HasAnOwner { address public owner; uint public a ; constructor() public { owner = msg.sender; } modifier ownerOnly(address addr) { require(addr == owner); //代码修饰器所修饰函数的代码 _; } function useSuperPowers() ownerOnly(msg.sender) public { a = 10; // do something only the owner should be allowed to do } }
继承
Solidity通过复制包括多态的代码来⽀持多重继承。
所有函数调⽤是虚拟(virtual)的,这意味着最远的派⽣⽅式会被调⽤,除⾮明确指定了合约。
当⼀个合约从多个其它合约那⾥继承,在区块链上仅会创建⼀个合约,在⽗合约⾥的代码会复制来形成继承
合约。
表达式和控制结构不⽀持switch和goto,⽀持if,else,while,do,for,break,continue,return,?: