pike语言语法

Pike Tutorial Study

Pike是一门解释型的、面向对象的编程语言,与C和C++有点像,但是更加容易学习和使用。它能够被用于小脚本或者是大型系统的编写。

Pike的主要特性有:

高级并强力、面向对象、解释性、最快的脚本语言、垃圾收集机制、易于扩展(C或者C++)

Pike的安装

1、去Pike官网上去下载安装包。

2、解压缩,直接make && make install

3、新建一个文件filename.pike,输入:

int main()
{
  write("Hello world!\n");
  return 0;
}

在命令行输入pike filename.pike,可以看到运行结果。

运行在窗口中

运行代码:

int main()
{
  GTK.setup_gtk();
  GTK.Alert("Hello world!")
    -> signal_connect("destroy", lambda(){ exit(0); });
  return -1;
}

效果如下:

windows

Pike的模块

Pike和大多数其他语言一样,提供了丰富的API给我们开发丰富的应用。比如说,当我们需要访问一个网页的时候,可以使用Protocols.HTTP模块下的Query对象。

形如:

void handle_url(string this_url){    
    write("Fetching URL '" + this_url + "'...");
    Protocols.HTTP.Query web_page; 
    web_page = Protocols.HTTP.get_url(this_url); 
    if(web_page == 0) { 
        write(" Failed!\n"); 
        return; 
    } 
    write(" Done.\n");
} 
// handle_url

当然,pike提供了import关键字完整导入一个模块。这样使用模块内部的类时就不用写上前缀了:

import Protocols.HTTP;
Query web_page;

当用web_page访问了一个网站之后,如果想要将网站的内容显示出来,则使用web_page提供的一个data()方法。

web_page->data()

基本数据类型

pike的基本数据类型包括:int float和string。

容器类型包括:array, mapping,multiset等

容器类型

最简单的容器类型是array。你可以往这个容器里面放入数据项,array是一个连续的大小的盒子。

array是一个形如这样的list:

({ "geegle", "boogle", "fnordle blordle" })
({ 12, 17, -3, 8 })
({  })
({ 12, 17, 17, "foo", ({ "gleep", "gleep" }) })

可以看到,除了基本类型,array还能嵌套其他容器类型。

能够这样子定义一个变量:

array a;        // Array of anything
array(string) b;    // Array of strings
array(array(string)) c;    // Array of arrays of strings
array(mixed) d;        // Array of anything

可以看到最后一行的array(mixed) d,这其实是一个多态容器,能够存放任意类型的数据。

然后就是array的赋值了:

a = ({ 17, "hello", 3.6 });
b = ({ "foo", "bar", "fum" });
c = ({ ({ "John", "Sherlock" }), ({ "Holmes" }) });
d = c;

我们可以访问数组一样通过下标访问array:

write(a[0]);    // Writes 17
b[1] = "bloo";    // Replaces "bar" with "bloo"
c[1][0] = b[2];    // Replaces "Holmes" with "fum"

有一些有意思的操作可以运用于array,比如说加法:

({ 7, 1 }) + ({ 3, 1, 6 })

({ 7, 1, 3, 1, 6 })

除了array还有两种容器类型:mapping和multiset。

set用来判断一个值是不是一个集合的成员。multiset则运行集合里面存在一样的值。

mapping有时被称为字典,通过键值对来存储数据。后面还会进一步解释。

方法也是对象

方法也是对象,这完全符合面向对象里面万物皆对象的理念。因此有时我们可以把方法像一个对象一样传递参数给另一个方法。比如说:

void write_one(int x)
{
  write("Number: " + x + "\n");
}

int main()
{
  array(int) all_of_them = ({ 1, 5, -1, 17, 17 });
  map(all_of_them, write_one);
  return 0;
}

Python里面也有这种特性,对于编程来说非常方便。

0是一个特别的东西

0这个值是一个特别的东西,不单单只是int类型的0,它可以代表所有的类型的“NULL”,如果我们创造了一个变量但是我们不想赋值它,默认会被赋值为0。

举个栗子:

string accumulate_answer()
{
  string result;
  // Probably some code here
  result += "Hi!\n";
  // More code goes here
  return result;
}
``

程序会返回一个`0Hi!\n`,前面多了一个0,这是我们常见的错误。因此,为了避免犯错,应该在变量声明的时候赋初值,哪怕是这样:`string result = ""`也好。

pike支持一个任意数据类型mixed,这意味着用mixed声明的变量可以存储任意的类型:

```pike
mixed x;
array(mixed) build_array(mixed x) { return ({ x }); }

pike还很神奇的支持或语句来声明变量类型,形如:

int|string w;

array(int|string|float) make_array(int|string|float x)
{
  return ({ x });
}

比较好的做好是声明一个明确的变量类型,这样编译器会帮助做类型检查,使得类型错误在编译时就被发现。

基本类型和引用类型

熟悉C++的话自然就知道引用时啥了。

基本数据类型都不是引用类型。int i1=5;i2=i1;在内存中像这样:

非引用

非基本数据类型比如说array就是引用类型,比如说下面的代码:

array(int) a1;
a1 = ({1,2,3})
array(int) a2;
a2 = a1;

在内存中会是像这样:

引用类型

当然如果不想要引用,而是值复制的话(这种方式更加推荐,避免数据同步问题。)

a2 = copy_value(a1)

内存中是这样的:

非引用

如果想要复制但是又不想用copy_value函数的话,可以这样子写:

a2 = a1 + ({})

返回的是一个a1的拷贝,与copy_value效果相同。

引用类型到底有哪些呢?下面列出来:

array
mapping
multiset
program
object
function

创建方法

pike在这方面的语法和c很像,一个简单的例子:

float average(float x1, float x2)
{
  return (x1 + x2) / 2;
}

方法的调用:

float x = average(19.0 + 11.0, 10.0);
average(27.13, x + 27.15);
float y = average(1.0, 2.0) + average(6.0, 7.1);
float z = average(1.0, average(2.0, 3.0));

两个更加复杂的例子:

int getDex()
{
  int oldDex = Dex;
  Dex = 0;
  return oldDex;
}

private void
show_user(int|string id, void|string full_name)
{
  write("Id: " + id + "\n");
  if(full_name)
    write("Full name: " + full_name + "\n");
}

如果想要调用:

getDex();
show_user(19);
show_user("john");
show_user(19, "John Doe");
show_user("john", "John Doe");

pike的多态

对象的使用

pike声明并初始化一个对象是这样的:

animal some_animal;
some_animal = animal();
animal my_dog = animal();

通过->调用对象的内部变量:

my_dog->name = "Fido";
my_dog->weight = 10.0;
some_animal->name = "Glorbie";
write("My dog is called " + my_dog->name + ".\n");
write("Its weight is " + my_dog->weight + ".\n");
write("That animal is called " + some_animal->name + ".\n");

初始化一个对象时可以调用有参构造函数:

animal piglet = animal("Piglet", 6.3);
my_dog->eat("quiche"); // Real dogs eat quiche.
write("Its weight is now " + my_dog->weight + ".\n");

从上面可以看到如果想要调用对象方法,依然是使用->。

类定义

话不多说,先上代码:

class animal
{
  string name;
  float weight;

  void create(string n, float w)
  {
    name = n;
    weight = w;
  }

  void eat(string food)
  {
    write(name + " eats some " + food + ".\n");
    weight += 0.5;
  }
}

分析一下,基本上与C++的类定义差不多,主要的区别在于没有声明访问控制(public和private)。

当然也可以把类当做结构体来用:

class customer
{
  int number;
  string name;
  array(string) phone_numbers;
}

调用代码:

array(customer) all_customers = ({ });
customer c = customer();
c->number = 18;
c->name = "Ellen Ripley";
c->phone_numbers = ({ "555-8767", "555-4001" });
all_customers += ({ c });

奇特用法

一个pike文件就是一个类。这是pike比较特殊的地方。

比如说我们创建一个"animal.pike"文件并输入:

string name;

float weight;

void create(string n, float w)
{
  name = n;
  weight = w;
}

void eat(string food)
{
  write(name + " eats some " + food + ".\n");
  weight += 0.5;
}

通过下面的方式把上面的这个文件当做一个类来用:

constant animal = (program)"animal.pike";
animal piglet = animal("Piglet", 6.3);

继承

多态的基础就是继承。继承能够带来代码重用,多态能够在运行时确定代码段的调用,解除代码耦合并带来更好的扩展性和灵活性。

比如说我们前面定义的动物类,可以作为父类衍生出下面这些子类:

class bird
{
  inherit animal;
  float max_altitude;

  void fly()
  {
    write(name + " flies.\n");
  }

  void eat(string food)
  {
    write(name + " flutters its wings.\n");
    ::eat(food);
  }
}

class fish
{
  inherit animal;
  float max_depth;

  void swim()
  {
    write(name + " swims.\n");
  }
}

可以像这样调用两个新的子类:

bird tweety = bird("Tweety", 0.13);
tweety->eat("corn");
tweety->fly();
tweety->max_altitude = 180.0;

fish b = fish("Bubbles", 1.13);
b->eat("fish food");
b->swim();

animal w = fish("Willy", 4000.0);
w->eat("tourists");
w->swim();

访问控制

pike类的成员变量默认是public的,这并不是一个好的做法。比较好的做法是,隐藏不该公开的所有信息。

值得注意的是:pike的static跟C++不一样,pike的static意味着成员只能被自己或者子类访问。这其实类似于protect。local变量声明的方法,意味着尽管被子类重载了,但是在这个类中还是使用这个本地的方法。

语句

语句包括有if、switch、for、while、do-while等。

基本语法与C相同,感觉无需赘述了。

写代码时,三元表达式是一个不错的选择,max_value = (a > b) ? a : b;非常用,而且理解上也不难。

foreach语句是一个不错的工具,可以用来遍历一组数据进行操作。形如:

foreach( container , loop-variable) 
statement

容器放前面,迭代器放后面,这似乎与其他语言的顺序是相反的。

有三种方法提前离开循环:

使用return
使用throw
使用exit终止程序

特殊语句

空语句

while(!finish())
;

catch语句

形如:

 mixed result = catch
  {
    i = klooble() + 2;
    fnooble();
    j = 1/i;
  };

  if(result == 0)
    write("Everything was ok.\n");
  else
    write("Oops. There was an error.\n");

后面的错误处理模块会更多的讨论catch语句。

更多关于数据类型的事情

int

如果想要判断变量是否为int,用intp(??)。

如果想要一个随机数,用random(limit)。

如果想要翻转bit,那么使用reverse(int)。需要注意的是,int是有符号的32位整数,因此reverse一个正数会转变为一个负数。

float

同理:

floatp -- 判断类型

四舍五入:

floor -- 下舍入,去掉小数点后面的数字。配合(int)f,转换为正数。

ceil -- 上舍入,也就是说不管小数点后面是啥都往前进位。

string

注意pike的string都是双引号的。

pike的string使用了著名的慢拷贝技术,剩下的你懂的。

同理:

stringp -- 判断类型

array

前面对于array讨论了很多。看看例子差不多了:

array(string) b;     // Array of strings
b = ({ "foo", "bar", "fum" });
b[1] = "bloo";       // Replaces "bar" with "bloo"

array(string) a1;       // a1 contains 0
a1 = ({ });             // Now a1 contains an empty array
array(int) a2 = ({ });  // a2 contains an empty array

write(a[0]);
b[1] = "bloo";
c[1] = b[2];

array(array(int)) aai = ({
  ({ 7, 9, 8 }),
  ({ -4, 9 }),
  ({ 100, 1, 2, 4, 17 })
});

array(string) a = ({ "foo", "bar" });
array(string) b = a;
array(string) c = ({ "foo", "bar" });

同理:

arrayp -- 判断类型

截取:({ 1, 7, 3, 3, 7 })[ 1..3 ] = ({ 7, 3, 3 })

array(int) a = ({ 7, 1 }); a == b;会返回0,及时a和b是相等的,但是由于使用的是完全不同的两块内存,也就是地址不同,所以其实不相等。

真正比较两个array看起来是否相等用equal。

支持集合操作:

| -- 联合
& -- 交集

  • -- A排除B的元素
    ^ -- 排除两方都有的元素
    / -- 用B把A切分成array的array,比如:
    ({ 7, 1, 2, 3, 4, 1, 2, 1, 2, 77 }) / ({ 1, 2 }) gives the result ({ ({ 7 }), ({ 3, 4 }), ({ }), ({ 77 }) }) .
    size 数量
    allocate(size) 预分配
    reverse 翻转
    search 返回第一个找到的元素位置,如果不存在返回-1
    has_value 这个只判断是否存在这个元素
    replace(array, old, new) 替换(用==)

mapping

先来看一些代码熟悉一下mapping:

([ "beer" : "cerveza", "cat" : "gato", "dog" : "perro" ])

mapping(string:string) m;
mapping(int:float) mif = ([ 1:3.6, -19:73.0 ]);
mapping(string:string) english2spanish = ([
  "beer" : "cerveza",
  "cat" : "gato",
  "dog" : "perro"
]);

mapping(string:float) m1;  // m1 contains 0
m1 = ([ ]);   // Now m1 contains an empty mapping
mapping(int:int) m2 = ([ ]);
              // m2 contains an empty mapping

write(english2spanish["cat"]); // Prints "gato"
english2spanish["dog"] = "gato";
    // Now, english2spanish["dog"] is "gato" too
english2spanish["beer"] = english2spanish["cat"];
    // Now, all values are "gato"

看起来其实就是Python里面的字典类型吧。通过键值对来存放数据。当然C++的STL里面也有mapping,大致和这个是一样的,区别在于pike的mapping定义时需要用([])扩住,有点麻烦。

mapping里面的顺序是无关系的,估计主要原因是以B树的形式存储,进入容器后会被重新排序。

如果想要查找一个mapping中不存在的key,那么value会返回0:

english2spanish["cat"]     // Gives "gato"
english2spanish["glurble"] // Gives 0

注意如果放入mapping的是一个对象的话,会有一些问题:

mapping(array(int) : int) m = ([ ]);
array(int) a = ({ 1, 2 });
m[a] = 3; //ok
write(m[({ 1, 2 })]) //error

mapping也提供了一些函数:

mappingp(some) :检查是否mapping

== 和 equal:==判断两个是否指向同一个东西,equal判断两个是否元素都相同。

indices():返回一个包含所有key的array

values():返回一个包含所有value的array

mkmapping(index-array, value-array):通过一个现成的index array和value array来创建mapping。

mapping1|mapping2:就是数学里的“联合”。如果两边都有的话(可能value不同),右手边优先。"+"是"union"的同义词啦。

mapping1 & mapping2:两边都有的才会返回,且如果value不一致,右手边优先。

mapping1 - mapping2:将mapping1中不在mapping中的元素给挑出来

mapping1 ^ mapping2:异或的作用是找到两个mapping的不同点。

sizeof(mapping):返回mapping的size

search(haystack, needle):逆向查找value的key,如果有多个也只返回第一个。如果不存在会返回编译error。

replace(mapping, old, new):替换旧的value为新的value

zero_type(mapping[index]):查询一个index是否存在于mapping当中。如果存在则返回一个0。不存在返回一个非0。

multiset

set就是集合,包含一堆数据,且数据不重复。如果需要重复数据的话,就要用multiset,这种数据类型用 (< >)来包裹。

还是那样的,如果只是这样multiset(string) m1;,m1就只是个0而已。如果想要的是空集合的话,应该这样才对:m1 = (< >);

直接看代码了解一下multiset的基本使用先:

if(dogs["Fido"])
  write("Fido is one of my dogs.\n");
if(!dogs["Dirk"])
  write("Dirk is not one of my dogs.\n");
dogs["Kicker"] = 1; // Add Kicker to the set
dogs["Buster"] = 0; // Remove Buster

multiset支持集合并和减操作,比如说:

dogs |= (< "Kicker" >); // Add Kicker to the set
dogs -= (< "Buster" >); // Remove Buster

multiset跟mapping一样,也是没有顺序的,不同顺序的multiset使用equal的比较是一样的。

multiset提供的一些操作:

multisetp(multiset):判断是否是一个multiset

== 和 equal:和mapping相同啦

集合的操作:| - & ^ 和前面的mapping都差不多,不再赘述了。

其他的参数类型

program

感觉看不太懂,比较有用的可能就是通过类名来创建对象:

program-name() or program-name(arguments)

判断是不是一个程序:

programp(something)

object

感觉没有太好说的,看函数吧:

objectp(some):判断some是否为一个object

根据类名创建一个对象:program-name() or program-name(arguments)

destruct(object):析构一个对象,如果对象里面有一个destroy方法,会被优先调用。一般来说不用显示的destroy一个对象,pike有自动的垃圾回收器。

访问一个对象:和C++访问一个指针是一样的。

function

看代码:

void write_one(int x)
{
  write("Number: " + x + "\n");
}

int main()
{
  array(int) all_of_them = ({ 1, 5, -1, 17, 17 });
  map(all_of_them, write_one);
  return 0;
}

function w = write;
w("Hello!\n");

提供的客户端:

functionp:判断是否是一个函数

function_name(funciton_object):得到方法对象的名字。

字符串

字符串的操作方法

==/+/-/连接/index/切片(({ 1, 7, 3, 3, 7 }) [ 1..3 ]

除法

比较特别的是string的除法:

"abcdfoofoo x" / "foo"  == ({ "abcd", "", " x" });

abcdfoofoo x" / 5 ==  ({ "abcdf", "oofoo" });

"abcdfoofoo x" / 5.0 ==  ({ "abcdf", "oofoo", " x" });

关于除以int和float的区别,上面的代码一眼就能看出来。

取模

看代码一眼就懂了:

"abcdfoofoo x" % 5 == " x";

乘法

看代码:

({ "7", "1", "foo" })  ":" == "7:1:foo";

string 的内置函数

stringp(something):判断一个对象是否是字符串

sizeof/replace/lower_case/upper_case/String.capitalize(string)/

search(haystack, needle):比如search("sortohum", "orto") == 1;

sprintf格式化生成字符串

用到直接看连接:sprintf格式化字符串

sscanf格式化提取字符串

用到直接看连接:sscanf提取字符串

宽字符串

就是除了8bit字符串之外的都叫宽字符串啦(中文编码这些)。

String.width(string):检测是否包含宽字符串

string_to_utf8(string data):转换编码为utf8

utf8_to_string(string utf8_encoded_data):转为原生的pike编码

表达式

基本的加减乘除取模不再赘述了。

可查通过查询得到隐式类型变换:隐式类型变换表

对于位操作符,可通过查表:位操作符表格

切片操作:

array(int) iii = ({1,2,3,4,5});
iii[1..3] = ({2,3,4})

赋值

值得留意的是群体赋值:

int i;
string s;
float f1, f2;

[ i, s, f1, f2 ] = ({ 3, "hi", 2.2, 3.14 });

类型转换

跟c差不多:

(float)14    // Gives the float 14.0
(int)6.9    // Gives the int 6
(string)6.7    // Gives the string "6.7"
(float)"6.7"    // Gives the float 6.7

pike支持一个特殊的类型:mixed的。这个类型能够存储任何类型的数据。

如果需要对于mixed类型做类型转换:

mixed m = 8;
int i = [int]m;

逗号操作符

总是返回逗号右边的那个值:

7 + 3, 6 * 2 // Gives 12
3,14         // Gives 14 (and not pi)

call and splice

如果我想把一个array里面的元素作为参数传给函数怎么做?(好奇怪的想法..)

array a = ({ 17, -0.3, "foo" });
koogle(@a);

equivalent

koogle(17, -0.3, "foo");

优先级

运算符的优先级可以通过查询表格:运算符优先级

预处理

c++推荐使用const完全代替macro的使用。我觉得pike也应该是这样的。

#define MAX_TEMP 100当然可以用,但是并不推荐。

const MAX_TEMP = 100是更好的选择。

当然,预处理也可以写一些简单的函数:

#define square(x) x * x

但是这样子写函数要注意,macro是直接替换的,而不是调用函数栈,所以容易存在算数优先级不明确的问题。

最好不要用include。用真正的继承比较好(这里不太明白???):

inherit .somefile;

macro比较能用的地方时声明源文件的字符编码:

#charset <*charset-name*>

可能这也是为什么没有废掉macro的原因所在吧。

默认的字符集是 iso8859-1,可以换成utf-8或者iso2022.

神奇的预处理常量

LINE:表示现在处于哪一行

DATA:变成month day year的时间

TIME:变成24小时制的时间:hh:mm:ss

模块

模块是一系列的插件。甚至可以用c或者c++来写。

pike提供了一系列的模块:

Stdio:标准输入输出

GTK:图形图像

Image:图片处理

Protocols:网络络协议

MIME:用来编码和解码MIME

Crypto:加密(我喜欢)

Calendar:日历

Sql:数据库操作相关

Thread:多线程

Process:多进程

Getopt:命令行参数

LR:语法分析器

Yp:网络信息系统的支持

Gz:解压缩文件

Regexp:正则表达式的匹配

如何使用这些模块

比如说标准输入:

string name = Stdio.stdin->gets();

比如说网络:

Protocols.HTTP.Query web_page;

如果我们不想写前缀:

import Stdio;
import Protocols.HTTP;

如果两个模块的函数名有冲突,会使用最新import的模块,谨记,这种错误很难被发现,所以要特别小心!!!

创建模块

用c和c++来创建模块不再这个讨论范围内,让我们来定义一个简单的模块吧:

创建一个trig.pmod文件。

输入:

float cos2(float f)
{
  return pow(cos(f), 2.0);
}

constant PI = 3.1415926535;

如何使用这些自定义模块呢?

int main()
{
  write("The square of cos(pi/4) is " +
    .trig.cos2(.trig.PI/4) + ".\n");
  return 0;
}

当然也可以导入后再用:

import .trig; // Import the module from this directory

int main()
{
  write("The square of cos(pi/4) is " +
    cos2(PI/4) + ".\n");
  return 0;
}

pike是如何找模块的?

大概遵循这个步骤:

如果前缀有.表示在本地目录下查找

如果有路径,包含路径名

在add_module_path中的路径

在命令行中用-M 指定的路径

在环境变量中的PIKE_MODULE_PATH

内置模块目录中的:/usr/local/pike/7.4.1/lib/modules/ or C:\Program Files\Pike\lib\pike\modules

pike是如何找到这个文件的呢?

查找到module-name.pmod文件,拷贝

查找到module-name.pmod目录,创建一个包含目录下所有文件的文件出来

查找到module-name.pmod.pike文件,拷贝

查找到module-name.so,文件会动态的链接c和c++编译的库。

错误处理

pike包括warning和error两种类型的警告。

waring通常不会显示出来,通过声明#pragma strict_types才能显示warning。

或者也可以再运行pike的时候这样:

pike -rT myprogram.pike

效果一样的。

pike里面,空对象会默认赋值为0。所以使用对象前都需要判断一下是否为0.

处理错误的方式:

1、终止程序并打印错误

if(result_of_something == 0)
{
  werror("Failed to open the file called '" +
         file_name + "'\n");
  exit(1);
}

2、输出错误到文件里面

webbrowser.pike cod.ida.liu.se > output.txt

错误码

不同类型的错误有不同的错误码。

比如说文件打开失败的错误:

Stdio.File localfile = Stdio.File();
if(!localfile->open(file_name, "r"))
{
  werror("Couldn't open '" + file_name + "'.\n");
  werror(strerror(localfile->errno()) +
         " (errno = " + localfile->errno() + ").\n");
  exit(1);
}

通过localfile->errno()来获取错误码。

catch-throw

这部分可能无需赘述,直接看代码吧:

if(result_of_something == 0)
  throw("Failed to open the file called '" +
        file_name + "'\n");

  mixed result = catch {
    i = klooble() + 2;
    fnooble();
    j = 1/i;
  };
  if(result == 0)
    write("Everything was ok.\n");
  else
    write("Oops. There was an error.\n");

void drink_coffee()
{
  if(coffe_pot == 0)
    throw("No coffe-pot.");
}

void eat_dinner()
{
  eat_main_course();
  eat_dessert();
  drink_coffee();
}

int main()
{
  mixed result = catch {
    eat_dinner();
  };
  if(result == 0)
    write("Everything was ok.\n");
  else
    write("There was an error: " + result + "\n");

  return 0;
}

int eat_dinner()
{
  if(eat_main_course() == 0)
    return 0;
  if(eat_dessert() == 0)
    return 0;
  if(drink_coffee() == 0)
    return 0;
  return 1;
}

mixed result = catch {
  koogle(0, 3.0, "foo");
};

if(result == 0)
  write("Everything was ok.\n");
else if(objectp(result))
{
  write("There was an error.\n");
  write("Type of error: " + result->error_type + "\n");
  write("Description of the error:\n");
  write(result->describe());
  write("Done.\n");
}
else {
  write("There was some other type of error.\n");
}


作者:1angxi
链接:https://www.jianshu.com/p/a1c0f554aa6e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值