前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >ES系列之嵌套文档和父子文档

ES系列之嵌套文档和父子文档

作者头像
用户7634691
发布于 2020-08-10 08:05:27
发布于 2020-08-10 08:05:27
4.3K00
代码可运行
举报
运行总次数:0
代码可运行

需求背景

很多时候mysql的表之间是一对多的关系,比如订单表和商品表。一笔订单可以包含多个商品。他们的关系如下图所示。

ElasticsSearch(以下简称ES)处理这种关系虽然不是特别擅长(相对于关系型数据库),因为ES和大多数 NoSQL 数据库类似,是扁平化的存储结构。索引是独立文档的集合体。不同的索引之间一般是没有关系的。

不过ES目前毕竟发展到7.x版本了, 已经有几种可选的方式能够高效的支持这种一对多关系的映射。

比较常用的方案是嵌套对象,嵌套文档和父子文档。后两种是我们本文要讲的重点。

我下面聚合分析使用的数据都是kibana自带的,这样方便有些读者实际测试文中的示例。

ES处理一对多关系的方案

普通内部对象

kibana自带的电商数据就是这种方式,我们来看看它的mapping。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
"kibana_sample_data_ecommerce" : {
    "mappings" : {
      "properties" : {
        "category" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword"
            }
          }
        },
        "currency" : {
          "type" : "keyword"
        },
        "customer_full_name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        //省略部分

        "products" : {
          "properties" : {
            "_id" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            },
            "base_price" : {
              "type" : "half_float"
            },
            "base_unit_price" : {
              "type" : "half_float"
            },
            "category" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword"
                }
              }
            },
            "created_on" : {
              "type" : "date"
            },
            "discount_amount" : {
              "type" : "half_float"
            },
            "discount_percentage" : {
              "type" : "half_float"
            },
            "manufacturer" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword"
                }
              }
            },
            "min_price" : {
              "type" : "half_float"
            },
            "price" : {
              "type" : "half_float"
            },
            "product_id" : {
              "type" : "long"
            },
            "product_name" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword"
                }
              },
              "analyzer" : "english"
            },
            "quantity" : {
              "type" : "integer"
            },
            "sku" : {
              "type" : "keyword"
            },
            "tax_amount" : {
              "type" : "half_float"
            },
            "taxful_price" : {
              "type" : "half_float"
            },
            "taxless_price" : {
              "type" : "half_float"
            },
            "unit_discount_amount" : {
              "type" : "half_float"
            }
          }
        },
        "sku" : {
          "type" : "keyword"
        },
        "taxful_total_price" : {
          "type" : "half_float"
        },
        //省略部分

我们可以看到电商的订单索引里面包含了一个products的字段,它是对象类型,内部有自己的字段属性。这其实就是一个包含关系,表示一个订单可以有多个商品信息。我们可以查询下看看结果,

查询语句,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST kibana_sample_data_ecommerce/_search
{
  "query": {
    "match_all": {}
  }
}

返回结果(我去掉了一些内容方便观察),

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
"hits" : [
      {
        "_index" : "kibana_sample_data_ecommerce",
        "_type" : "_doc",
        "_id" : "VJz1f28BdseAsPClo7bC",
        "_score" : 1.0,
        "_source" : {
          "customer_first_name" : "Eddie",
          "customer_full_name" : "Eddie Underwood",
          "order_date" : "2020-01-27T09:28:48+00:00",
          "order_id" : 584677,
          "products" : [
            {
              "base_price" : 11.99,
              "discount_percentage" : 0,
              "quantity" : 1,
              "sku" : "ZO0549605496",
              "manufacturer" : "Elitelligence",
              "tax_amount" : 0,
              "product_id" : 6283,
            },
            {
              "base_price" : 24.99,
              "discount_percentage" : 0,
              "quantity" : 1,
              "sku" : "ZO0299602996",
              "manufacturer" : "Oceanavigations",
              "tax_amount" : 0,
              "product_id" : 19400,
            }
          ],
          "taxful_total_price" : 36.98,
          "taxless_total_price" : 36.98,
          "total_quantity" : 2,
          "total_unique_products" : 2,
          "type" : "order",
          "user" : "eddie",
            "region_name" : "Cairo Governorate",
            "continent_name" : "Africa",
            "city_name" : "Cairo"
          }
        }
      },

可以看到返回的products其实是个list,包含两个对象。这就表示了一个一对多的关系。

这种方式的优点很明显,由于所有的信息都在一个文档中,查询时就没有必要去ES内部没有必要再去join别的文档,查询效率很高。那么它优缺点吗?

当然有,我们还用上面的例子,如下的查询,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET kibana_sample_data_ecommerce/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "products.base_price": 24.99 }},
        { "match": { "products.sku":"ZO0549605496"}},
        {"match": { "order_id": "584677"}}
      ]
    }
  }
}

我这里搜索有三个条件,order_id,商品的价格和sku,事实上同时满足这三个条件的文档并不存在(sku=ZO0549605496的商品价格是11.99)。但是结果却返回了一个文档,这是为什么呢?

原来在ES中对于json对象数组的处理是压扁了处理的,比如上面的例子在ES存储的结构是这样的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
  "order_id":            [ 584677 ],
  "products.base_price":    [ 11.99, 24.99... ],
  "products.sku": [ ZO0549605496, ZO0299602996 ],
  ...
}

很明显,这样的结构丢失了商品金额和sku的关联关系。

如果你的业务场景对这个问题不敏感,就可以选择这种方式,因为它足够简单并且效率也比下面两种方案高。

嵌套文档

很明显上面对象数组的方案没有处理好内部对象的边界问题,JSON数组对象被ES强行存储成扁平化的键值对列表。为了解决这个问题,ES推出了一种所谓的嵌套文档的方案,官方对这种方案的介绍是这样的:

The nested type is a specialised version of the object datatype that allows arrays of objects to be indexed in a way that they can be queried independently of each other.

可以看到嵌套文档的方案其实是对普通内部对象这种方案的补充。上面那个电商的例子mapping太长了,我换个简单一些的例子,只要能说明问题就行了。

先设置给索引设置一个mapping,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT test_index
{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested" 
      }
    }
  }
}

user属性是nested,表示是个内嵌文档。其它的属性这里没有设置,让es自动mapping就可以了。

插入两条数据,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT test_index/_doc/1
{
  "group" : "root",
  "user" : [
    {
      "name" : "John",
      "age" :  30
    },
    {
      "name" : "Alice",
      "age" :  28
    }
  ]
}

PUT test_index/_doc/2
{
  "group" : "wheel",
  "user" : [
    {
      "name" : "Tom",
      "age" :  33
    },
    {
      "name" : "Jack",
      "age" :  25
    }
  ]
}

查询的姿势是这样的,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET test_index/_search
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.name": "Alice" }},
            { "match": { "user.age":  28 }} 
          ]
        }
      }
    }
  }
}

注意到nested文档查询有特殊的语法,需要指明nested关键字和路径(path),再来看一个更具代表性的例子,查询的条件在主文档和子文档都有。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET test_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "group": "root"
          }
        },
        {
          "nested": {
            "path": "user",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "user.name": "Alice"
                    }
                  },
                  {
                    "match": {
                      "user.age": 28
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

说了这么多,似乎嵌套文档很好用啊。没有前面那个方案对象边界缺失的问题,用起来似乎也不复杂。那么它有缺点吗?当然,我们先来做个试验。

先看看当前索的文档数量,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET _cat/indices?v

查询结果,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
green  open   test_index                   FJsEIFf_QZW4Q4SlZBsqJg   1   1          6            0     17.7kb          8.8kb

你可能已经注意到我这里查看文档数量并不是用的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET test_index/_count

而是直接查看的索引信息,他们的区别打算后面专门的文章讲解,现在你只需要知道前者可以看到底层真实的文档数量即可。

是不是很奇怪问啥文档的数量是6而不是2呢?这是因为nested子文档在ES内部其实也是独立的lucene文档,只是我们在查询的时候,ES内部帮我们做了join处理。最终看起来好像是一个独立的文档一样。

那可想而知同样的条件下,这个性能肯定不如普通内部对象的方案。在实际的业务应用中要根据实际情况决定是否选择这种方案。

父子文档

我们还是看上面那个例子,假如我需要更新文档的group属性的值,需要重新索引这个文档。尽管嵌套的user对象我不需要更新,他也随着主文档一起被重新索引了。

还有就是如果某个表属于跟多个表有一对多的关系,也就是一个子文档可以属于多个主文档的场景,用nested无法实现。

下面来看示例。

首先我们定义mapping,如下,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT my_index
{
  "mappings": {
    "properties": {
      "my_id": {
        "type": "keyword"
      },
      "my_join_field": { 
        "type": "join",
        "relations": {
          "question": "answer" 
        }
      }
    }
  }
}

my_join_field是给我们的父子文档关系的名字,这个可以自定义。join关键字表示这是一个父子文档关系,接下来relations里面表示question是父,answer是子。

插入两个父文档,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT my_index/_doc/1
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": {
    "name": "question" 
  }
}


PUT my_index/_doc/2
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": {
    "name": "question"
  }
}

"name": "question"表示插入的是父文档。

然后插入两个子文档

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT my_index/_doc/3?routing=1
{
  "my_id": "3",
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer", 
    "parent": "1" 
  }
}

PUT my_index/_doc/4?routing=1
{
  "my_id": "4",
  "text": "This is another answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

子文档要解释的东西比较多,首先从文档id我们可以判断子文档都是独立的文档(跟nested不一样)。其次routing关键字指明了路由的id是父文档1, 这个id和下面的parent关键字对应的id是一致的。

需要强调的是,索引子文档的时候,routing是必须的,因为要确保子文档和父文档在同一个分片上。

name关键字指明了这是一个子文档。

现在my_index中有四个独立的文档,我们来父子文档在搜索的时候是什么姿势。

先来一个无条件查询,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET my_index/_search
{
  "query": {
    "match_all": {}
  },
  "sort": ["my_id"]
}

返回结果(部分),

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : null,
        "_routing" : "1",
        "_source" : {
          "my_id" : "3",
          "text" : "This is an answer",
          "my_join_field" : {
            "name" : "answer",
            "parent" : "1"
          }
        },

可以看到返回的结果带了my_join_field关键字指明这是个父文档还是子文档。

Has Child 查询,返回父文档

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST my_index/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query" : {
                "match": {
                    "text" : "answer"
                }
            }
    }
  }
}

返回结果(部分),

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
"hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "my_id" : "1",
          "text" : "This is a question",
          "my_join_field" : {
            "name" : "question"
          }
        }
      }
    ]

Has Parent 查询,返回相关的子文档

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST my_index/_search
{
  "query": {
    "has_parent": {
      "parent_type": "question",
      "query" : {
                "match": {
                    "text" : "question"
                }
            }
    }
  }
}

结果(部分),

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "my_id" : "3",
          "text" : "This is an answer",
          "my_join_field" : {
            "name" : "answer",
            "parent" : "1"
          }
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "my_id" : "4",
          "text" : "This is another answer",
          "my_join_field" : {
            "name" : "answer",
            "parent" : "1"
          }
        }
      }
    ]

Parent Id 查询子文档

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST my_index/_search
{
  "query": {
    "parent_id": { 
      "type": "answer",
      "id": "1"
    }
  }
}

返回的结果和上面基本一样,区别在于parent id搜索默认使用相关性算分,而Has Parent默认情况下不使用算分。

使用父子文档的模式有一些需要特别关注的点:

  • 每一个索引只能定义一个 join field
  • 父子文档必须在同一个分片上,意味着查询,更新操作都需要加上routing
  • 可以向一个已经存在的join field上新增关系

总的来说,嵌套对象通过冗余数据来提高查询性能,适用于读多写少的场景。父子文档类似关系型数据库中的关联关系,适用于写多的场景,减少了文档修改的范围。

总结

  1. 普通子对象模式实现一对多关系,会损失子对象的边界,子对象的属性之前关联性丧失。
  2. 嵌套对象可以解决普通子对象存在的问题,但是它有两个缺点,一个是更新主文档的时候要全部更新,另外就是不支持子文档从属多个主文档的场景。
  3. 父子文档能解决前面两个存在的问题,但是它适用于写多读少的场景。

参考:

*《elasticsearch 官方文档》

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 犀牛的技术笔记 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
c#基础系列1---值类型和引用类型
不知不觉已经踏入坑已10余年之多,对于c#多多少少有一点自己的认识,写出来渴求同类抨击,对自己也算是个十年之痒的一个总结。
架构师修行之路
2019/07/23
8830
c#基础系列1---值类型和引用类型
[C#1] 3-基元类型、引用类型和值类型、装箱拆箱
1.基元类型 编译器直接支持的数据类型成为基元类型。基元类型与FCL中的类型有直接的映射关系[int=Int32],这样我们可以简化的方式书写代码,并且编译后的IL和直接使用FCL中的数据类型是完全相同的。 Checked和Unchecked操作: Byte b=100; b=(Byte)(b+200); CLR只在32位和64位上进行算数运算,所以b首先会被转换为32位的值再和100相加,得到的是32位的值,接着转型为Byte,再然后将其放入b的存储堆栈。但是b的结果是44,反生了溢出,并不是期望的300
blackheart
2018/01/19
1K0
.NET面试题解析(01)-值类型与引用类型
3. delegate是引用类型还是值类型?enum、int[]和string呢?
莫问今朝
2018/08/31
8650
.NET面试题解析(01)-值类型与引用类型
[读书笔记]C#学习笔记三: C#类型详解..
前言 这次分享的主要内容有五个, 分别是值类型和引用类型, 装箱与拆箱,常量与变量,运算符重载,static字段和static构造函数. 后期的分享会针对于C#2.0 3.0 4.0 等新特性进行. 再会有三篇博客  这个系列的就会结束了. 也算是自己对园子中@Learning Hard出版的<<C#学习笔记>>的一个总结了. 博客内容基本上都是白天抽空在公司写好的了, 但是由于公司内部网络不能登录博客园所以只能够夜晚拿回来修改,  写的不好或者不对的地方也请各位大神指出. 在下感激不尽了.  1,值类型和
一枝花算不算浪漫
2018/05/18
1.4K0
C#基础:理解装箱与拆箱
在C#编程语言中,装箱(Boxing)和拆箱(Unboxing)是与泛型编程和.NET Framework的公共语言运行时(CLR)的类型系统紧密相关的两个概念。这两个过程涉及到值类型(ValueType)和引用类型(ReferenceType)之间的转换,对于理解C#的内存管理和性能优化至关重要。本文将深入探讨装箱和拆箱的机制、使用场景以及相关的性能考量。
Michel_Rolle
2024/10/10
3K0
.NET面试题解析(02)-拆箱与装箱
装箱和拆箱几乎是所有面试题中必考之一,看上去简单,就往往容易被忽视。其实它一点都不简单的,一个简单的问题也可以从多个层次来解读。
莫问今朝
2018/08/31
5530
.NET面试题解析(02)-拆箱与装箱
【译】.NET中六个重要的概念:栈、堆、值类型、引用类型、装箱和拆箱
  一来是为了感受国外优秀技术社区知名博主的高质量文章,二来是为了复习对.NET技术的基础拾遗达到温故知新的效果,最后也是为了锻炼一下自己的英文读写能力。因为是首次翻译英文文章(哎,原谅我这个菜比,弱爆了!),所以肯定会有很多问题(有些语句理解不透彻,翻译出来也不通顺,还请不吝赐教),也请各位园友多多指正,谢谢!
Edison Zhou
2018/08/20
3960
【译】.NET中六个重要的概念:栈、堆、值类型、引用类型、装箱和拆箱
C#基础补充
A.对值类型的分配。 虚拟内存中存在一个叫堆栈的区域,我们并不知道它到底在地址空间的什么地方,在一般开发过程中也没有必要知道,我们知道的是值类型就分配于此。值类型在堆栈上分配的时候,是自上而下填充的,也就是从高内存地址开始填充。 比如当前的堆栈指针为100000,这表明它的下一个自由存储空间从99999开始,当我们在C#中声明一个int类型的变量A,因为int类型是四个字节,所以它将分配在99996到99999这个存储单元中。如果我们接着声明double变量B(8字节),该变量将分配在99988到99995这个存储单元。 如果代码运行到他们的作用域之外,这时候A和B两个变量都将被删除,此时的顺序正好相反,先删除变量B,同时堆栈指针会递增8,也就是重新指向到99996这个位置;接下来删除变量A,堆栈指针重新指向10000。如果两个变量是同时声明的。如int A,B,此时我们并不知道A和B的分配顺序,但是编译器会确保他们的删除顺序正好和分配顺序相反。
Echo_Wish
2023/11/30
2110
.NET基础拾遗(1)类型语法基础和内存管理基础
在.NET中所有的内建类型都继承自System.Object类型。在C#中,不需要显示地定义类型继承自System.Object,编译器将自动地自动地为类型添加上这个继承申明,以下两行代码的作用完全一致:
Edison Zhou
2018/08/20
7180
.NET基础拾遗(1)类型语法基础和内存管理基础
C#面试题
值类型包括简单类型、结构体类型和枚举类型,引用类型包括自定义类、数组、接口、委托等。 1、赋值方式:将一个值类型变量赋给另一个值类型变量时,将复制包含的值。这与引用类型变量的赋值不同,引用类型变量的赋值只复制对象的引用(即内存地址,类似C++中的指针),而不复制对象本身。 2、继承:值类型不可能派生出新的类型,所有的值类型均隐式派生自 System.ValueType。但与引用类型相同的是,结构也可以实现接口。 3、null:与引用类型不同,值类型不可能包含 null 值。然而,可空类型功能允许将 null 赋给值类型。 4、每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值,值类型初始会默认为0,引用类型默认为null。 5、值类型存储在栈中,引用类型存储在托管堆中。
全栈程序员站长
2022/09/07
9080
C# 学习笔记(8)—— 深入理解类型
C# 中的类型可以分为两种——值类型和引用类型,本文详细分析两种类型,并讨论它们之间的类型转换方法
Karl Du
2023/10/20
2720
值类型和引用类型的区别,struct和class的区别
Christal_R
2017/12/25
4.2K0
值类型和引用类型的区别,struct和class的区别
c#基础:值类型与引用类型2
但是想下 List<int> 中的 值 到底存放在堆上 还是栈上,所以我这边拆分成5个点来梳理:
洪移潮
2024/11/27
980
c# 误区系列(二)
这个是为什么呢?其实是这样子的,当泛型类型确认的时候,那么add 定义的时候就已经确定了类型。
梁规晓
2020/11/05
6690
c# 误区系列(二)
.NET面试题系列[4] - C# 基础知识(2)
面试出现频率:主要考察装箱和拆箱。对于有笔试题的场合也可能会考一些基本的类型转换是否合法。
s055523
2018/09/14
9540
.NET面试题系列[4] - C# 基础知识(2)
C# 堆与栈、值类型与引用类型、可空类型
栈是一种先进后出的数据结构,是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义。栈中每个指针(当运行到那个变量时)会指向堆中的某一内存区域或说是空间。 堆(heap)就直接是内存区域了,它是为了栈的引用而开发内存的。通常内置变量就是值类型是被保存在栈中的。其他由.NET框架(Framework)提供的,或者是我们自己定义的对象即引用类型,一般被创建在堆中并将由栈中变量引用。是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小。
aehyok
2019/02/25
1.1K0
老生常谈--什么是装箱什么是拆箱
我们知道.NET具有两个数据类型:值类型和引用类型。因为值类型没有指针引用,不是分配在托管堆中,也不会被GC回收,因此它比引用类型更加高效。但有时我们需要将一种类型的变量转换为另一种类型,这时我们就可以使用装箱/拆箱。
喵叔
2021/12/08
4890
C#系列之值类型和引用类型
    这几天一直在思考这章讨论什么, 在上一章讨论string的时候牵涉到引用类型,那么我们这一章讨论讨论一下,值类型和引用类型。
陈珙
2018/09/12
7990
C#系列之值类型和引用类型
关于 .NET 8 中装箱和取拆箱的不为人知的真相:每个 C# 开发人员都需要了解的内容
如果我告诉您,像装箱和取消装箱这样的简单概念可以决定 .NET 应用程序的性能,该怎么办?如果您认为装箱和取消装箱只是普通的 C# 功能,请再想一想。想象一下,只需进行一些调整即可优化代码,突然提高效率并减少内存开销。您知道装箱和拆箱中隐藏的陷阱如何悄无声息地导致重大问题吗?如果您有兴趣掌握 .NET 8,您应该继续阅读并了解如何让装箱和取消装箱为您服务。 简介:什么是装箱和拆箱?
郑子铭
2024/11/23
2110
关于 .NET 8 中装箱和取拆箱的不为人知的真相:每个 C# 开发人员都需要了解的内容
堆和栈的含义,值类型和引用类型
☞ 堆是无序的,是一片不连续的内存域,由用户自己来控制和释放,如果用户自己不释放的话,当内存达到一定的特定值时或程序运行结束时,通过垃圾回收器(GC)来回收。
zls365
2020/08/19
1.5K0
堆和栈的含义,值类型和引用类型
相关推荐
c#基础系列1---值类型和引用类型
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档