【c++】小项目cJSON研读(下)

cJSON.c

给各种接口分好类后,开始细看它们的实现。

除了cJSON.h以外用了一下头文件

#include <string.h>
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <limits.h>
#include <ctype.h>
#include <float.h>

一、内存管理相关

typedef struct internal_hooks
{
    void *(CJSON_CDECL *allocate)(size_t size);
    void (CJSON_CDECL *deallocate)(void *pointer);
    void *(CJSON_CDECL *reallocate)(void *pointer, size_t size);
} internal_hooks;
...
...
static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc };

定义了internal_hooks结构体,并创建了一个global_hooks用来管理所有的cJSON对象的内存分配allocate、回收deallocate和重新分配reallocate。三种内存管理的实现的方式是调用<stdlib.h>里的malloc、free、realloc。作者给你封装一下方便我们自定义自己的内存管理,比如说你要是想在cJSON对象销毁前,暂存这个对象的数据30天,便可在free前进行操作。

static void CJSON_CDECL internal_free(void *pointer)
{
    free(pointer);
}

二、创建Creat相关

CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean)
{
    cJSON *item = cJSON_New_Item(&global_hooks);
    if(item)
    {
        item->type = boolean ? cJSON_True : cJSON_False;
    }

    return item;
}

cJSON_CreateNull、cJSON_CreateTrue、cJSON_CreateFalse、cJSON_CreateBool、cJSON_CreateArray、cJSON_CreateObject均与以上代码类似,其中调用的cJSON_New_Item是使用自定义的hooks结构体里的allocate方法申请内存,在申请成功后,将内存全部置’\0’

/* Internal constructor. */
static cJSON *cJSON_New_Item(const internal_hooks * const hooks)
{
    cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON));
    if (node)
    {
        memset(node, '\0', sizeof(cJSON));
    }

    return node;
}

cJSON_CreateNumber和cJSON_CreateString、cJSON_CreateRaw除了对该cJSON对象的type赋值外,还需额外赋值valuedouble、valuestring。(很好理解吧,你创建一个数字或者字符串类型的cJSON对象,总要把该对象的值给记录下来吧)

除此之外还有一些实现的细节:

  • 对数字的创建,判断了数字是否在 INT_MIN 到 INT_MAX 之间 超出范围的直接设为两个边界值。
  • 对字符串的创建,调用了cJSON_strdup进行内存的拷贝。如果拷贝失败(返回NULL)则字符串cJSON也创建失败,销毁该item,返回NULL。
static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks)
{
    size_t length = 0;
    unsigned char *copy = NULL;

    if (string == NULL)
    {
        return NULL;
    }

    length = strlen((const char*)string) + sizeof(""); 
    copy = (unsigned char*)hooks->allocate(length); // 用hooks去申请一块地址
    if (copy == NULL)
    {
        return NULL;
    }
    memcpy(copy, string, length); // 拷贝

    return copy;
}

cJSON_CreateArrayReference 、 cJSON_CreateObjectReference 、 cJSON_CreateStringReference 均类似多了一步去除const关键字修饰(因为需要动态增长)

CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) {
    cJSON *item = cJSON_New_Item(&global_hooks);
    if (item != NULL) {
        item->type = cJSON_Array | cJSON_IsReference;
        item->child = (cJSON*)cast_away_const(child);  // cast_away_const
    }

    return item;
}

/* helper function to cast away const */
static void* cast_away_const(const void* string)
{
    return (void*)string;
}

拥有以上接口,可以实现创建各种头结点,而后调用添加接口,举例如下

cjson = cJSON_CreateArray();
cJSON_AddItemToArray(cjson, cJSON_CreateString( "C" ));
cJSON_AddItemToArray(cjson, cJSON_CreateString( "Java" ));

除了一个个手动添加数组元素外,还提供了用数组直接创建cJSON对象的四个接口。cJSON_CreateIntArray、cJSON_CreateFloatArray、cJSON_CreateDoubleArray、cJSON_CreateStringArray。其传入的参数均为两个,数组首地址和数组元素个数。以cJSON_CreateIntArray为例:

/* Create Arrays: */
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count)
{
    size_t i = 0;  // 用来迭代取数的
    cJSON *n = NULL;  // 新创建的Number类型的cJSON
    cJSON *p = NULL;  // 上一个Number;因为是双向链表实现的,所以需要记录上一个结点
    cJSON *a = NULL;  // Array类型的cJSON

    if ((count < 0) || (numbers == NULL))
    {   // 如果数组元素个数小于0,或者数组不存在
        return NULL;
    }
	// 创建数组cJSON
    a = cJSON_CreateArray();
	// 它真的好严谨啊,每次都会检查创建操作是否成功
    for(i = 0; a && (i < (size_t)count); i++)
    {
        n = cJSON_CreateNumber(numbers[i]); // DoubleArray、StringArray都类似
        if (!n)
        {   // 创建不成功啊
            cJSON_Delete(a);
            return NULL;
        }
        if(!i)
        {  // i=0时,即将array父结点child指向第一个number结点
            // 这里没有将首个number节点的prev指向array父结点
            a->child = n;
        }
        else
        {// 数组双向链表,链表内部用prev和next连接
            suffix_object(p, n);  
        }
        p = n;
    }
	// 这里将首个number结点的prev指向最后一个number节点,组成循环链表
    // 便于插入时直接找到链表末尾
    if (a && a->child) { 
        a->child->prev = n;
    }

    return a;
}

/* Utility for array list handling. */
static void suffix_object(cJSON *prev, cJSON *item)
{
    prev->next = item;
    item->prev = prev;
}

(假如我用c++来写能不能使用函数的重载、模板等方法来简化以上代码?毕竟它四种接口写了四遍啊,明明里面只有两句不同。)

三、添加Add相关

Add最核心两个函数 add_item_to_array 和 add_item_to_object

static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) // 静态函数
{
    cJSON *child = NULL;

    if ((item == NULL) || (array == NULL) || (array == item))
    {
        return false;
    }

    child = array->child;
    /*
     * To find the last item in array quickly, we use prev in array
     在cJSON_CreateIntArray里有看到过,
     父节点array里的首个child的prev指向的是最后一个元素
     */
    if (child == NULL)
    {
        /* list is empty, start new one */
        array->child = item;
        item->prev = item;
        item->next = NULL;
    }
    else
    {
        /* append to the end */
        if (child->prev)
        {
            suffix_object(child->prev, item); // 这个前面介绍过了
            array->child->prev = item;
        }
    }

    return true;
}
/*
函数解释:在object里面添加一个名为string的item,使用hooks来进行内存分配,constant_key来判断对于名字string的选择直接使用,还是拷贝副本使用。
*/
static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key)
{
    char *new_key = NULL;
    int new_type = cJSON_Invalid;

    if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item))
    {
        return false;
    }

    if (constant_key)
    {// 将传入的string 去掉它的const修饰直接作为名字标识
        new_key = (char*)cast_away_const(string);
        new_type = item->type | cJSON_StringIsConst;
    }
    else
    {// 拷个string副本
        new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks);
        if (new_key == NULL)
        {
            return false;
        }

        new_type = item->type & ~cJSON_StringIsConst;
    }

    if (!(item->type & cJSON_StringIsConst) && (item->string != NULL))
    {
        hooks->deallocate(item->string);
    }

    item->string = new_key;
    item->type = new_type;
	// 所以说更本质的还是add_item_to_array
    return add_item_to_array(object, item);
}

cJSON_AddItemToArray、cJSON_AddItemToObject、cJSON_AddItemToObjectCS,均是包装了一下,在对参数进行一些处理后都使用的是静态函数add_item_to_array进行添加。不同的是ToObeject和ToObjectCS,从逻辑上是将一个cJSON嵌入进另一个cJSON,所以多了一个参数string需要对待嵌入的cJSON对象命名,而涉及字符串的操作,便实现了两个版本:ToObeject是将string拷贝一份副本作为名字,ToObjectCS是直接将该字符串作为名字。

/* Add item to array/object. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item)
{
    return add_item_to_array(array, item); // 包装了一下 
}

其他的基本同理,均是对需要添加的Item进行创建或拷贝后将新的cJSON Add进去。

CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name)
{
    cJSON *array = cJSON_CreateArray();
    if (add_item_to_object(object, name, array, &global_hooks, false))
    {
        return array;
    }

    cJSON_Delete(array);
    return NULL;
}

四、分离Detach删除Delete更新Update

分离Detach也是在套娃,Delete更是直接先通过Detach分出来然后递归释放掉内存

CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which)
{ // 对用户友好一点,只要给出 array 和 待删数据的下标
    if (which < 0)
    {
        return NULL;
    }
	// 都是调cJSON_DetachItemViaPointer分离
    return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which));
}

cJSON_DetachItemViaPointer是从parent指向的链表数组中找item并且扒拉下来。很好理解不赘述了。

CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item)
{
    if ((parent == NULL) || (item == NULL))
    {
        return NULL;
    }

    if (item != parent->child)
    {
        /* not the first element */
        item->prev->next = item->next;
    }
    if (item->next != NULL)
    {
        /* not the last element */
        item->next->prev = item->prev;
    }

    if (item == parent->child)
    {
        /* first element */
        parent->child = item->next;
    }
    else if (item->next == NULL)
    {
        /* last element */
        parent->child->prev = item->prev;
    }

    /* make sure the detached item doesn't point anywhere anymore */
    item->prev = NULL;
    item->next = NULL;

    return item;
}

删除都是这个cJSON_Delete。cJSON如果嵌套了cJSON要递归删,cJSON如果是指针或者字符串类型需要释放对应的string

/* Delete a cJSON structure. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item)
{
    cJSON *next = NULL;
    while (item != NULL)
    {
        next = item->next; // 记录下一个要删的
        if (!(item->type & cJSON_IsReference) && (item->child != NULL))
        { // 如果该item是个嵌套的cJSON,要递归删child里面的
            cJSON_Delete(item->child);
        }
        if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL))
        {
            global_hooks.deallocate(item->valuestring);
        }
        if (!(item->type & cJSON_StringIsConst) && (item->string != NULL))
        {
            global_hooks.deallocate(item->string);
        }
        global_hooks.deallocate(item);
        item = next;
    }
}

Update实现了:

  • 在数组指定位置插入一个item——cJSON_InsertItemInArray
  • 在数组指定位置替换——cJSON_ReplaceItemInArray而后调用cJSON_ReplaceItemViaPointer
  • 在cJSON指定name里替换——cJSON_ReplaceItemInObject和其CS版本(同上面的Add 的CS版本作用一样)最终还是调到cJSON_ReplaceItemViaPointer这个函数
// 代码贴过来我累了不想看了
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement)
{
    if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL))
    {
        return false;
    }

    if (replacement == item)
    {
        return true;
    }

    replacement->next = item->next;
    replacement->prev = item->prev;

    if (replacement->next != NULL)
    {
        replacement->next->prev = replacement;
    }
    if (parent->child == item)
    {
        if (parent->child->prev == parent->child)
        {
            replacement->prev = replacement;
        }
        parent->child = replacement;
    }
    else
    {   /*
         * To find the last item in array quickly, we use prev in array.
         * We can't modify the last item's next pointer where this item was the parent's child
         */
        if (replacement->prev != NULL)
        {
            replacement->prev->next = replacement;
        }
        if (replacement->next == NULL)
        {
            parent->child->prev = replacement;
        }
    }

    item->next = NULL;
    item->prev = NULL;
    cJSON_Delete(item);

    return true;
}

五、解析Parse

解析一共有四个相关的,分别为

  • cJSON_ParseWithLengthOpts:最全面的,允许设置待解析的字符数组地址,长度,是否返回数组解析末尾,是否要求末尾不加’\0’;

  • cJSON_ParseWithOpts:相当于默认给你设置好了length,然后调用了上面这个WithLengthOpts

  • cJSON_ParseWithLength:相当于默认设好了Opts

  • cJSON_Parse:最偷懒的,你只要给一个字符数组给它,另外都默认给你掉

后面三个最终都是会调用到第一个,来看第一个接口

/* Parse an object - create a new root, and populate. */
/* 参数解释:并不是直接在value上进行解析,而是拷贝一份并添加长度等信息得到parse_buffer进行解析
value:形如“{"color":{"blue", "white"}}” 这样的char*数组,末尾可能有'\0',中间可能有空格
buffer_length:待解析的长度
return_parse_end:返回解析后buffer的位置
require_null_terminated:是否对buffer进行删空格等处理
*/
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated)
{
    // 专门定义了一个数据结构来存放待解析的字符串
    parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } };
/*parse_buffer长这样
typedef struct
{
    const unsigned char *content;
    size_t length;
    size_t offset;
    size_t depth; // 输入在数组或者对象的嵌套深度
    internal_hooks hooks;
} parse_buffer;
*/
    cJSON *item = NULL;

    /* reset error position */
    global_error.json = NULL;
    global_error.position = 0;

    if (value == NULL || 0 == buffer_length)
    {
        goto fail;
    }
	// 赋值
    buffer.content = (const unsigned char*)value;
    buffer.length = buffer_length;
    buffer.offset = 0;
    buffer.hooks = global_hooks;

    item = cJSON_New_Item(&global_hooks);
    if (item == NULL) /* memory fail */
    {
        goto fail;
    }
	// parse_value这里开始解析,详细见下个代码框
    if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer))))
    {
        /* parse failure. ep is set. */
        goto fail;
    }

    /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */
    // 可以这样子调用cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0);
    if (require_null_terminated)
    {// 对应第三个参数
        buffer_skip_whitespace(&buffer);
        if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0')
        {
            goto fail;
        }
    }
    if (return_parse_end)
    {// 对应第四个参数
        *return_parse_end = (const char*)buffer_at_offset(&buffer);
    }

    return item;

fail:// 越看越觉得好代码的细节真是考虑得周到
    if (item != NULL)
    {
        cJSON_Delete(item);
    }

    if (value != NULL)
    {
        error local_error;
        local_error.json = (const unsigned char*)value;
        local_error.position = 0;

        if (buffer.offset < buffer.length)
        {
            local_error.position = buffer.offset;
        }
        else if (buffer.length > 0)
        {
            local_error.position = buffer.length - 1;
        }

        if (return_parse_end != NULL)
        {
            *return_parse_end = (const char*)local_error.json + local_error.position;
        }

        global_error = local_error;
    }

    return NULL;
}

parse_value的代码,根据value的type不同进行解析,’123‘得解析为数字吧,’abc‘得解析为sting吧。挑两个出来看看:type为number和obeject的。

/* Parser core - when encountering text, process appropriately. */
static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer)
{
    if ((input_buffer == NULL) || (input_buffer->content == NULL))
    {
        return false; /* no input */
    }

    /* parse the different types of values */
    ...
    ...
    /* number */
    if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9'))))
    {
        return parse_number(item, input_buffer);
    }
   	...
    ...
    /* object */
    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{'))
    {
        return parse_object(item, input_buffer);
    }

    return false;
}

都用到了can_access_at_indexbuffer_at_offset。这两个是宏函数,预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程。简单来说就是提升效率。作用从名字中可以看出来。

#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length))

#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset)

还调用了各自的parse_numberparse_object

/* Parse the input text to generate a number, and populate the result into item. */
static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer)
{
    double number = 0;
    unsigned char *after_end = NULL;
    unsigned char number_c_string[64];
    unsigned char decimal_point = get_decimal_point();// 它小数点都考虑到了不同地区的小数点还不同,在<locale.h>中,感兴趣看
    // https://www.runoob.com/cprogramming/c-standard-library-locale-h.html
    size_t i = 0;

    if ((input_buffer == NULL) || (input_buffer->content == NULL))
    {
        return false;
    }

    /* copy the number into a temporary buffer and replace '.' with the decimal point
     * of the current locale (for strtod)
     * This also takes care of '\0' not necessarily being available for marking the end of the input */
    for (i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++)
    {// 这里在把“123.346”拆开成一个一个的放入number_c_string中
        switch (buffer_at_offset(input_buffer)[i])
        {
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '+':
            case '-':
            case 'e':// 牛逼还考虑指数
            case 'E':
                number_c_string[i] = buffer_at_offset(input_buffer)[i];
                break;

            case '.':
                number_c_string[i] = decimal_point;
                break;

            default:
                goto loop_end;
        }
    }
loop_end:
    number_c_string[i] = '\0'; // c风格的字符串补个\0
	// strtod是<stdlib.h>库里面的
    number = strtod((const char*)number_c_string, (char**)&after_end);
    if (number_c_string == after_end)
    {
        return false; /* parse_error */
    }

    item->valuedouble = number;

    /* 溢出的处理 */
    if (number >= INT_MAX)
    {
        item->valueint = INT_MAX;
    }
    else if (number <= (double)INT_MIN)
    {
        item->valueint = INT_MIN;
    }
    else
    {
        item->valueint = (int)number;
    }

    item->type = cJSON_Number;
	// 更新offset
    input_buffer->offset += (size_t)(after_end - number_c_string);
    return true;
}

parse_object实在是长的有点可怕了,全贴过来显得会很臃肿,故删掉了一些判断内存是否分配成功这些异常处理的代码。

static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer)
{
    cJSON *head = NULL; /* linked list head */
    cJSON *current_item = NULL;
    
    input_buffer->depth++;  // depth是标识嵌套深度的
    input_buffer->offset++; // ++是因为首个字符一定是{ 
    // 跳过空格,相当于找到第一个有效字符
    buffer_skip_whitespace(input_buffer);

    /* step back to character in front of the first element */
    input_buffer->offset--;
    /* loop through the comma separated array elements */
    do
    {
        /* allocate next item */
        cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks));

        /* attach next item to list */
        if (head == NULL)
        {
            /* start the linked list */
            current_item = head = new_item;
        }
        else
        {
            /* add to the end and advance */
            current_item->next = new_item;
            new_item->prev = current_item;
            current_item = new_item;
        }

        /* parse the name of the child 一定是string */
        input_buffer->offset++;
        buffer_skip_whitespace(input_buffer);
        // 解析出来子对象的名字
        if (!parse_string(current_item, input_buffer))
        {
            goto fail; /* failed to parse name */
        }
        buffer_skip_whitespace(input_buffer);
		// 因为调用的parse_string本来是用来解析string类型
        // 专门赋值作为valuestring而不是作为名字的
        /* swap valuestring and string, because we parsed the name */
        current_item->string = current_item->valuestring;
        current_item->valuestring = NULL;

        /* parse the value */
        input_buffer->offset++;
        buffer_skip_whitespace(input_buffer);
        // 你看这就是开始递归调用前面介绍过的parse_value
        if (!parse_value(current_item, input_buffer))
        {
            goto fail; /* failed to parse value */
        }
        buffer_skip_whitespace(input_buffer);
    }
    while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); // do-while 循环

success: // fail 的被我删了,可以自己去看
    input_buffer->depth--; // 嵌套层数--,虽然目前我还没看出来记录个嵌套层数有什么用

    if (head != NULL) {
        head->prev = current_item;
    }

    item->type = cJSON_Object;
    item->child = head;

    input_buffer->offset++;
    return true;

}

六、获取Get输出Print

获取Get不就是简单版的删除Delete嘛,找出来,不用删罢了。

输出Print的各自接口,主要是静态函数print,它定义了printbuffer等等(前面是pares_buffer,这个又不加下划线了)然后交给静态函数print_value去根据type将value放到printbuffer里面。

static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks)
/* Render a value to text. */
static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer)
{
    unsigned char *output = NULL;

    if ((item == NULL) || (output_buffer == NULL))
    {
        return false;
    }

    switch ((item->type) & 0xFF) // 没懂这么&一下是干嘛
    {
        case cJSON_NULL:
            output = ensure(output_buffer, 5);
            if (output == NULL)
            {
                return false;
            }
            strcpy((char*)output, "null");
            return true;

        case cJSON_False:
            output = ensure(output_buffer, 6);
            if (output == NULL)
            {
                return false;
            }
            strcpy((char*)output, "false");
            return true;

        case cJSON_True:
            output = ensure(output_buffer, 5);
            if (output == NULL)
            {
                return false;
            }
            strcpy((char*)output, "true");
            return true;

        case cJSON_Number:
            return print_number(item, output_buffer);

        case cJSON_Raw:
        {
            size_t raw_length = 0;
            if (item->valuestring == NULL)
            {
                return false;
            }

            raw_length = strlen(item->valuestring) + sizeof("");
            output = ensure(output_buffer, raw_length);
            if (output == NULL)
            {
                return false;
            }
            memcpy(output, item->valuestring, raw_length);
            return true;
        }

        case cJSON_String:
            return print_string(item, output_buffer);

        case cJSON_Array:
            return print_array(item, output_buffer);

        case cJSON_Object: // 脑子痛起来了
            return print_object(item, output_buffer);

        default:
            return false;
    }
}

总结

其实看到Get和Print接口时开始看不进去了,因为发现都大同小异——在双向循环可嵌套的链表上增删改查。在把数据结构设计好后(cJSON,buffer,hooks,error等)剩下的就是细心和耐心考虑多种来自用户的不规范输入、系统时不时内存分配不成功(string,及各类指针)、数据溢出、编码格式等等细节,出现异常后撤销分配的内存等已经执行的操作。在代码结构上,对用户仅提供了可以调用的接口,真正对cJSON链表进行操作的代码是作为静态函数隐藏起来的。

看完这些代码最直观的一个感受是 它的异常处理部分特别的完善。3k行可能2k都在处理异常。respect

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值