首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API

使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API

作者头像
solenovex
发布于 2018-04-19 10:47:47
发布于 2018-04-19 10:47:47
1.3K00
代码可运行
举报
文章被收录于专栏:草根专栏草根专栏
运行总次数:0
代码可运行

上一篇写的是使用静态基类方法的实现步骤: http://www.cnblogs.com/cgzl/p/8726805.html

使用dynamic (ExpandoObject)的好处就是可以动态组建返回类型, 之前使用的是ViewModel, 如果想返回结果的话, 肯定需要把ViewModel所有的属性都返回, 如果属性比较多, 就有可能造成性能和灵活性等问题. 而使用ExpandoObject(dynamic)就可以解决这个问题.

返回一个对象

返回一个dynamic类型的对象, 需要把所需要的属性从ViewModel抽取出来并转化成dynamic对象, 这里所需要的属性通常是从参数传进来的, 例如针对下面的CustomerViewModel类, 参数可能是这样的: "Name, Company":

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
using System;
using SalesApi.Core.Abstractions.DomainModels;

namespace SalesApi.ViewModels
{
    public class CustomerViewModel: EntityBase
    {
        public string Company { get; set; }
        public string Name { get; set; }
        public DateTimeOffset EstablishmentTime { get; set; }
    }
}

还需要一个Extension Method可以把对象按照需要的属性转化成dynamic类型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace SalesApi.Shared.Helpers
{
    public static class ObjectExtensions
    {
        public static ExpandoObject ToDynamic<TSource>(this TSource source, string fields = null)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            var dataShapedObject = new ExpandoObject();
            if (string.IsNullOrWhiteSpace(fields))
            {
                // 所有的 public properties 应该包含在ExpandoObject里 
                var propertyInfos = typeof(TSource).GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                foreach (var propertyInfo in propertyInfos)
                {
                    // 取得源对象上该property的值
                    var propertyValue = propertyInfo.GetValue(source);
                    // 为ExpandoObject添加field
                    ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
                }
                return dataShapedObject;
            }

            // field是使用 "," 分割的, 这里是进行分割动作.
            var fieldsAfterSplit = fields.Split(',');
            foreach (var field in fieldsAfterSplit)
            {
                var propertyName = field.Trim();

                // 使用反射来获取源对象上的property
                // 需要包括public和实例属性, 并忽略大小写.
                var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                if (propertyInfo == null)
                {
                    throw new Exception($"没有在‘{typeof(TSource)}’上找到‘{propertyName}’这个Property");
                }

                // 取得源对象property的值
                var propertyValue = propertyInfo.GetValue(source);
                // 为ExpandoObject添加field
                ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
            }

            return dataShapedObject;
        }
    }
}

注意: 这里的逻辑是如果没有选择需要的属性的话, 那么就返回所有合适的属性.

然后在CustomerController里面:

首先创建为对象添加link的方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        private IEnumerable<LinkViewModel> CreateLinksForCustomer(int id, string fields = null)
        {
            var links = new List<LinkViewModel>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                links.Add(
                    new LinkViewModel(_urlHelper.Link("GetCustomer", new { id = id }),
                    "self",
                    "GET"));
            }
            else
            {
                links.Add(
                    new LinkViewModel(_urlHelper.Link("GetCustomer", new { id = id, fields = fields }),
                    "self",
                    "GET"));
            }

            links.Add(
                new LinkViewModel(_urlHelper.Link("DeleteCustomer", new { id = id }),
                "delete_customer",
                "DELETE"));

            links.Add(
                new LinkViewModel(_urlHelper.Link("CreateCustomer", new { id = id }),
                "create_customer",
                "POST"));

            return links;
        }

针对返回一个对象, 添加了本身的连接, 添加的连接 以及 删除的连接.

然后修改Get和Post的Action:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        [HttpGet]
        [Route("{id}", Name = "GetCustomer")]
        public async Task<IActionResult> Get(int id, string fields)
        {
            var item = await _customerRepository.GetSingleAsync(id);
            if (item == null)
            {
                return NotFound();
            }
            var customerVm = Mapper.Map<CustomerViewModel>(item);
            var links = CreateLinksForCustomer(id, fields);
            var dynamicObject = customerVm.ToDynamic(fields) as IDictionary<string, object>;
            dynamicObject.Add("links", links);
            return Ok(dynamicObject);
        }

        [HttpPost(Name = "CreateCustomer")]
        public async Task<IActionResult> Post([FromBody] CustomerViewModel customerVm)
        {
            if (customerVm == null)
            {
                return BadRequest();
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var newItem = Mapper.Map<Customer>(customerVm);
            _customerRepository.Add(newItem);
            if (!await UnitOfWork.SaveAsync())
            {
                return StatusCode(500, "保存时出错");
            }

            var vm = Mapper.Map<CustomerViewModel>(newItem);

            var links = CreateLinksForCustomer(vm.Id);
            var dynamicObject = vm.ToDynamic() as IDictionary<string, object>;
            dynamicObject.Add("links", links);

            return CreatedAtRoute("GetCustomer", new { id = dynamicObject["Id"] }, dynamicObject);
        }

红色部分是相关的代码. 创建links之后把vm对象按照需要的属性转化成dynamic对象. 然后往这个dynamic对象里面添加links属性. 最后返回该对象.

下面测试一下.

POST:

结果:

由于POST方法里面没有选择任何fields, 所以返回所有的属性.

下面试一下GET:

再试一下GET, 选择几个fields:

OK, 效果都如预期.

但是有一个问题, 因为返回的json的Pascal case的(只有dynamic对象返回的是Pascal case, 其他ViewModel现在返回的都是camel case的), 而camel case才是更好的选择 .

所以在Startup里面可以这样设置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
            services.AddMvc(options =>
            {
                options.ReturnHttpNotAcceptable = true;
                // the default formatter is the first one in the list.
                options.OutputFormatters.Remove(new XmlDataContractSerializerOutputFormatter());

                // set authorization on all controllers or routes
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                options.Filters.Add(new AuthorizeFilter(policy));
            })
            .AddJsonOptions(options =>
            {
                options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            })
            .AddFluetValidations();

然后再试试:

OK.

返回集合

 首先编写创建links的方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        private IEnumerable<LinkViewModel> CreateLinksForCustomers(string fields = null)
        {
            var links = new List<LinkViewModel>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                links.Add(
                   new LinkViewModel(_urlHelper.Link("GetAllCustomers", new { fields = fields }),
                   "self",
                   "GET"));
            }
            else
            {
                links.Add(
                   new LinkViewModel(_urlHelper.Link("GetAllCustomers", new { }),
                   "self",
                   "GET"));
            }
            return links;
        }

这个很简单.

然后需要针对IEnumerable<T>类型创建把ViewModel转化成dynamic对象的Extension方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace SalesApi.Shared.Helpers
{
    public static class IEnumerableExtensions
    {
        public static IEnumerable<ExpandoObject> ToDynamicIEnumerable<TSource>(this IEnumerable<TSource> source, string fields)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            var expandoObjectList = new List<ExpandoObject>();
            var propertyInfoList = new List<PropertyInfo>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                var propertyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
                propertyInfoList.AddRange(propertyInfos);
            }
            else
            {
                var fieldsAfterSplit = fields.Split(',');
                foreach (var field in fieldsAfterSplit)
                {
                    var propertyName = field.Trim();
                    var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                    if (propertyInfo == null)
                    {
                        throw new Exception($"Property {propertyName} wasn't found on {typeof(TSource)}");
                    }
                    propertyInfoList.Add(propertyInfo);
                }
            }

            foreach (TSource sourceObject in source)
            {
                var dataShapedObject = new ExpandoObject();
                foreach (var propertyInfo in propertyInfoList)
                {
                    var propertyValue = propertyInfo.GetValue(sourceObject);
                    ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
                }
                expandoObjectList.Add(dataShapedObject);
            }

            return expandoObjectList;
        }
    }
}

注意: 反射的开销很大, 注意性能.

然后修改GetAll方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        [HttpGet(Name = "GetAllCustomers")]
        public async Task<IActionResult> GetAll(string fields)
        {
            var items = await _customerRepository.GetAllAsync();
            var results = Mapper.Map<IEnumerable<CustomerViewModel>>(items);
            var dynamicList = results.ToDynamicIEnumerable(fields);
            var links = CreateLinksForCustomers(fields);
            var dynamicListWithLinks = dynamicList.Select(customer =>
            {
                var customerDictionary = customer as IDictionary<string, object>;
                var customerLinks = CreateLinksForCustomer(
                    (int)customerDictionary["Id"], fields);
                customerDictionary.Add("links", customerLinks);
                return customerDictionary;
            });
            var resultWithLink = new {
                Value = dynamicListWithLinks,
                Links = links
            };
            return Ok(resultWithLink);
        }

红色部分是相关代码.

测试一下:

不选择属性:

选择部分属性:

OK.

HATEOAS这部分就写到这.

其实 翻页的逻辑很适合使用HATEOAS结构. 有空我再写一个翻页的吧.

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-04-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
LoRA及其变体概述:LoRA, DoRA, AdaLoRA, Delta-LoRA
LoRA可以说是针对特定任务高效训练大型语言模型的重大突破。它被广泛应用于许多应用中。在本文中,我们将解释LoRA本身的基本概念,然后介绍一些以不同的方式改进LoRA的功能的变体,包括LoRA+、VeRA、LoRA- fa、LoRA-drop、AdaLoRA、DoRA和Delta-LoRA。
deephub
2024/03/20
4.3K0
LoRA及其变体概述:LoRA, DoRA, AdaLoRA, Delta-LoRA
每日论文速递 | BiLoRA: 基于双极优化消除LoRA过拟合
摘要:低秩适应(LoRA)是在下游任务中通过学习低秩增量矩阵对大规模预训练模型进行微调的一种流行方法。虽然与完全微调方法相比,LoRA 及其变体能有效减少可训练参数的数量,但它们经常会对训练数据进行过拟合,导致测试数据的泛化效果不理想。为了解决这个问题,我们引入了 BiLoRA,这是一种基于双级优化(BLO)的消除过拟合的微调方法。BiLoRA 采用伪奇异值分解来参数化低秩增量矩阵,并将伪奇异向量和伪奇异值的训练分成两个不同的训练数据子集。这种分割嵌入了 BLO 框架的不同层次,降低了对单一数据集过度拟合的风险。BiLoRA 在涵盖自然语言理解和生成任务的十个数据集上进行了测试,并应用于各种著名的大型预训练模型,在可训练参数数量相似的情况下,BiLoRA 明显优于 LoRA 方法和其他微调方法。
zenRRan
2024/04/11
5930
每日论文速递 | BiLoRA: 基于双极优化消除LoRA过拟合
大模型关于Lora论文集合
论文地址:https://arxiv.org/pdf/2401.04151.pdf
致Great
2024/01/12
6290
大模型关于Lora论文集合
北航&北大 | 提出统一微调框架,整合前沿微调方法,可支持100多种LLMs的微调!
当前开源社区中有许多的大模型,为了能够将其适配至不同应用场景,基本上都需要精心的调整模型参数。为了能够实现对大模型的高效微调,本文作者提出了一个统一的大模型微调框架:LLAMAFACTORY,该框架整合了一系列前沿的高效微调方法,支持对100多种大模型的微调。
ShuYini
2024/03/26
8970
北航&北大 | 提出统一微调框架,整合前沿微调方法,可支持100多种LLMs的微调!
6种大模型微调技术
由于LLM参数量都是在亿级以上,少则数十亿,多则数千亿。当我们想在用特定领域的数据微调模型时,如果想要full-tuning所有模型参数,看着是不太实际,一来需要相当多的硬件设备(GPU),二来需要相当长的训练时间。
皮大大
2025/05/07
4610
在消费级GPU调试LLM的三种方法:梯度检查点,LoRA和量化
LLM的问题就是权重参数太大,无法在我们本地消费级GPU上进行调试,所以我们将介绍3种在训练过程中减少内存消耗,节省大量时间的方法:梯度检查点,LoRA和量化。
deephub
2023/08/30
1.3K0
在消费级GPU调试LLM的三种方法:梯度检查点,LoRA和量化
每日论文速递 | GaLore: 使用梯度低秩映射进行大模型 Memory-Efficient 全参训练
摘要:训练大型语言模型(LLMs)面临着显著的内存挑战,主要是由于权重和优化器状态的不断增大。常见的内存降低方法,如低秩适应(LoRA),在每一层中向冻结的预训练权重添加一个可训练的低秩矩阵,从而减少可训练参数和优化器状态。然而,这些方法通常在预训练和微调阶段的性能上都不如使用全秩权重训练,因为它们将参数搜索限制在低秩子空间中,改变了训练动态,并且可能需要全秩热启动。在这项工作中,我们提出了Gradient Low-Rank Projection(GaLore),一种允许全参数学习但比LoRA等常见低秩适应方法更节省内存的训练策略。我们的方法在优化器状态的内存使用上最多减少了65.5%,同时在使用C4数据集进行LLaMA 1B和7B架构的预训练以及在GLUE任务上对RoBERTa进行微调时,保持了效率和性能。我们的8位GaLore相较于BF16基准,将优化器内存进一步降低了82.5%,总训练内存降低了63.3%。值得注意的是,我们首次证明了在具有24GB内存的消费级GPU上(例如NVIDIA RTX 4090)进行7B模型的预训练是可行的,而无需模型并行、检查点策略或卸载策略。
zenRRan
2024/03/14
6150
每日论文速递 | GaLore: 使用梯度低秩映射进行大模型 Memory-Efficient 全参训练
一文搞懂!如何高效微调你的 LLM
当前以 ChatGPT 为代表的预训练语言模型(PLM)规模变得越来越大,在消费级硬件上进行全量微调(Full Fine-Tuning)变得不可行。此外,为每个下游任务单独存储和部署微调模型变得非常昂贵,因为微调模型与原始预训练模型的大小相同。
NewBeeNLP
2023/08/29
2.4K0
一文搞懂!如何高效微调你的 LLM
ICML 2024 | 脱离LoRA架构,训练参数大幅减少,新型傅立叶微调来了
本文介绍了香港科技大学(广州)的一篇关于大模型高效微调(LLM PEFT Fine-tuning)的文章「Parameter-Efficient Fine-Tuning with Discrete Fourier Transform」,本文被 ICML 2024 接收,代码已开源。
机器之心
2024/06/03
3590
ICML 2024 | 脱离LoRA架构,训练参数大幅减少,新型傅立叶微调来了
一文带你了解当前主流PEFT技术
随着LLaMA3的发布,大模型开源社区的战力又提升了一分,国内目前应该已经有不少大佬已经开始着手对LLaMA3进行研究或微调,对于微调技术,目前比较常见的就是Peft系列的技术,那么什么是PEFT,有哪些分类,为什么这么受大家欢迎呢?今天我们就好好聊聊这个话题。
叶子的技术碎碎念
2025/04/08
4770
一文带你了解当前主流PEFT技术
华为提出QA-LoRA:让微调大型语言模型‘轻装上阵’
近些年,大型语言模型(LLMs)表现出了强大的语言理解能力。但是,想要将这些模型应用到不同的场景,并部署到边缘设备(例如手机)上,我们还面临一个重要的问题:这些模型通常参数众多,计算负担重,如何在不损失性能的情况下让它们变得“轻量”并适应不同需求呢?
zenRRan
2023/10/09
1.4K0
华为提出QA-LoRA:让微调大型语言模型‘轻装上阵’
LowRA框架实现每参数低于2 Bits LoRA微调,内存降50%,释放受限环境潜力!
在人工智能(AI)领域,随着技术的飞速发展,研究者们对AI算法、模型及其应用的研究日益深入。本文旨在探讨AI在各个领域的应用现状、挑战与发展趋势,以期为我国AI产业的发展提供有益的参考。
未来先知
2025/03/13
1470
LowRA框架实现每参数低于2 Bits LoRA微调,内存降50%,释放受限环境潜力!
一篇关于LLM指令微调的综述
指令微调(IT)是提高大型语言模型(LLM)能力和可控性的关键技术。其本质是指在由(INSTRUCTION, OUTPUT)对组成的数据集上以监督的方式进一步训练LLM的过程,它弥合了LLM的下一个词预测目标与用户让LLM遵循人类指令的目标之间的差距。这篇文章对现有研究进行了系统的回顾、包括IT的一般方法、IT数据集的构建、IT模型的训练、以及不同模式,领域和应用的应用。
zenRRan
2023/09/11
7.2K0
一篇关于LLM指令微调的综述
大模型的模型压缩与有效推理综述
本文对大型语言模型的压缩和效率推理进行了综述。大型语言模型基于Transformer架构,具有强大的性能,但也带来了巨大的内存和计算成本。本文从算法角度对大型语言模型的压缩和效率推理方法进行了分类,包括量化、剪枝、知识蒸馏、紧凑架构设计和动态网络。大型语言模型有两个显著特点:
算法进阶
2024/07/10
7550
大模型的模型压缩与有效推理综述
每日论文速递 | AutoLoRA:通过meta learning学习LoRA最优秩
摘要:在各种 NLP 任务中,大规模预训练和针对特定任务的微调取得了巨大成功。由于对大型预训练模型的所有参数进行微调会带来巨大的计算和内存挑战,人们开发出了几种高效的微调方法。其中,低秩适应(Low-rank adaptation,LoRA)在冻结的预训练权重基础上对低秩增量更新矩阵进行微调,已被证明特别有效。然而,LoRA 在所有层中统一分配秩,并依赖穷举搜索来找到最佳秩,这导致了高计算成本和次优的微调性能。为了解决这些局限性,我们引入了 AutoLoRA,这是一种基于元学习的框架,用于自动识别每个 LoRA 层的最佳等级。AutoLoRA 将低秩更新矩阵中的每个秩-1 矩阵与一个选择变量相关联,该选择变量决定是否应丢弃秩-1 矩阵。我们开发了一种基于元学习的方法来学习这些选择变量。通过对这些变量的值进行阈值化处理,确定最佳秩。我们在自然语言理解、生成和序列标注方面的综合实验证明了 AutoLoRA 的有效性。
zenRRan
2024/03/25
4960
每日论文速递 | AutoLoRA:通过meta learning学习LoRA最优秩
每日论文速递 | 当缩放遇到LLM微调:数据、模型和微调方法的影响
摘要:虽然大型语言模型(LLM)通常采用微调来解锁其下游应用程序的功能,但我们对不同微调方法的归纳偏差(特别是缩放属性)的理解仍然有限。为了填补这一空白,我们进行了系统的实验,研究不同的缩放因子,包括LLM模型大小,预训练数据大小,新的微调参数大小和微调数据大小,是否以及如何影响微调性能。我们考虑两种类型的微调-全模型调整(FMT)和参数有效的调整(PET,包括即时调整和LoRA),并探讨其缩放行为的数据有限的制度,其中LLM模型的大小大大超过微调的数据大小。基于1B到16 B两组预训练的双语LLM,以及在双语机器翻译和多语种摘要基准测试上的实验,我们发现:1)LLM微调遵循基于幂的乘法联合缩放律,即微调数据大小与彼此缩放因子之间的比例关系; 2)LLM微调从LLM模型缩放中获得的收益大于预训练数据缩放,PET参数缩放通常无效;以及3)最优微调方法是高度任务和微调数据相关的。我们希望我们的研究结果可以帮助理解,选择和发展LLM微调方法。
zenRRan
2024/03/02
6360
每日论文速递 | 当缩放遇到LLM微调:数据、模型和微调方法的影响
恐怖如斯!GSU | 提出VB-LoRA,仅需LoRA参数的0.4%,就超越了LoRA微调效果
随着大模型应用的不断推广,面对不同应用场景模型的定制化需求也不断增涨。但参数高效微调 (PEFT) 方法,比如LoRA及其变体会产生大量的参数存储和传输成本。为此,本文提出了一种超级参数高效微调方法:VB-LoRA,该方法采用“分而共享(divide-and-share)”范式,通过向量库进行全局参数共享,在保证模型性能的同时,实现了极高的参数效率。在对 Llama2-13B 模型进行微调时,VB-LoRA 仅使用了 LoRA 存储参数的 0.4%就超过了LoRA微调效果,可见实力强悍。
ShuYini
2024/05/30
5050
恐怖如斯!GSU | 提出VB-LoRA,仅需LoRA参数的0.4%,就超越了LoRA微调效果
从0到1!得物如何打造通用大模型训练和推理平台
近期,GPT大模型的发布给自然语言处理(NLP)领域带来了令人震撼的体验。随着这一事件的发生,一系列开源大模型也迅速崛起。依据一些评估机构的评估,这些开源模型大模型的表现也相当不错。一些大模型的评测情况可以去这里查询:Huggingface的Open LLM排行榜,UC伯克利发布大语言模型排行榜等。
得物技术
2023/07/31
1.5K0
从0到1!得物如何打造通用大模型训练和推理平台
解读LoRA
大模型调优(finetuning)不仅仅是参数的优化,同样会受到非功能性约束的挑战,例如:
半吊子全栈工匠
2023/12/20
1.1K0
解读LoRA
碾压LoRA!Meta & CMU | 提出高效大模型微调方法:GaLore,内存可减少63.3%
大模型训练通常会遇到内存资源的限制。目前常用的内存减少方法低秩适应(LoRA),通过引入低秩(low-rank)适配器来更新模型的权重,而不是直接更新整个权重矩阵。然而,这种方法在预训练和微调阶段通常表现不佳,为此,本文作者提出了梯度低秩映射(Gradient Low-Rank Projection ,「GaLore」),这是一种允许「全参数」学习的训练策略,并且比 LoRA 等常见的低秩适应方法更节省内存,相比BF16内存减少了63.3% 。
ShuYini
2024/03/11
1.2K0
碾压LoRA!Meta & CMU | 提出高效大模型微调方法:GaLore,内存可减少63.3%
推荐阅读
相关推荐
LoRA及其变体概述:LoRA, DoRA, AdaLoRA, Delta-LoRA
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验