复合(或复杂)属性,即 Complex Property,怎么理解呢?这是相对于常见的基础类型,比如 string、int、byte、long 等类型的属性就是基础类型值,而由多个基础类型构成的类型就是复合类型(当然,复合类型的属性也可以其他复合类型,这里就不套娃了)。
比如,某顿饭由米饭、菜、肉、汤组成。
public class 大餐
{
public MiFan 饭 { get; set;} // 复合
public Tang 汤 { get; set; } // 复合
public CaiRou 主菜 { get; set; } // 复合
}
public class MiFan
{
public string 原料 { get; set; } // 用什么煮的
public string 单位 { get; set; } // 千克
public float 量 { get; set; } // 吃多少饭
}
public class Tang
{
public string 主材 { get; set; } // 用什么煲的汤
public string? 配材 { get; set; } // 配哪些辅料
public float 水用量 { get; set; } // 用多少水
public DateTime 用时 { get; set; } // 煮了多久
}
public class CaiRou
{
public string 分类 { get; set; } // 肉还是青菜
public double? 分量 { get; set; } // 吃多少
public bool? 是否混合 { get; set; } // 是菜肉混合还是吃素的
}
其中,饭、汤、主菜几个属性都引用了其他的类,就是复合类型。这时候你会发现:咦,这怎么看着像导航属性?
是的,很像,但只是很像而已,实际上是有区别的。老周总结如下:
1、导航属性是一个实体引用另一个实体,强调的是引用和被引用者都是实体类;而复合属性所引用的实例并不作为实体类来看待,纯粹是复杂一点的类型;
2、正因为导航属性引用的实体类实例,所以,被引用的类型通常要有主键的(特殊配置除外)。复合属性引用的对象不需要主键;
3、导航属性引用的对象是实体,它会映射到数据库中的表。复合属性引用的对象不会做表映射(只成为主表的字段,后文会演示);
4、由于导航属性引用的是实体,所以它可以是集合类型。而复合类型没有映射单独的表,而是被拆解为主表的字段,所以它引用的对象不能是集合(至少目前是不支持的)。
那么,应该在什么时候,或什么场景下使用复合属性(而不是导航属性)?一句话斯基:你只想用一个类来组织一些信息,但不希望它成为实体的时候。有大伙伴会质问老周:你不是废话吗。这不是废话,本来就是这个用途的。
例如,一个比 SARS 还典型的应用场景------我的产品订单信息中会包含订货者的信息,但这个信息用一个字段描述显然不够,因为它包含联系人是谁、什么电话号码、住在哪里。当然你也可以单独用一个表来存放联系人信息,如果这样它就是实体类了,就要用导航属性。可是,有些时候是没必要单独去存储的(比如,订你货的人或单位不是常客,何必多建一张表呢)。
光说不练等于白说,咱们就动手试试。
假如我有个 Person 实体类,它可以表示任意人员(员工、学生、罪犯、外星人),其中,Contact 属性表示联系方式。这个信息量超出单个字段的描述范围(用XML或JSON字段的当我没说),于是,我另外定义了一个 ContactInfo 类存放联系信息。
public class ContactInfo
{
public string Phone { get; set; } = string.Empty;
public string? Email { get; set; }
public string City { get; set; } = string.Empty;
public string Town { get; set; } = "";
public string? Street { get; set; }
}
public class Person
{
public Guid ID { get; set; }
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public DateOnly? RegistDate { get; set; }
public ContactInfo Contact { get; set; } = new();
}
目前来说,EF Core 是不允许复合属性引用 null 值的,即不能声明为 ContactInfo?。但复合类型中的属性是可以为 null 的。
咱们写个 DbContext 来建立模型。
public class MyContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
"Server=(localdb)\\MsSQLLocalDB; Database=person_db"
)
.*LogTo(m*=\> Console.WriteLine(m)); // 启用日志
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>(eb =>
{
// 表名
eb.ToTable("tb_persons")
.HasKey(x => x.ID).HasName("PK_Person");
// 个别字段限制一下长度
eb.Property(x => x.FirstName).HasMaxLength(10);
eb.Property(nameof(Person.LastName)).HasMaxLength(25);
// 复合属性
eb.ComplexProperty(x =\>**x.Contact);**
});
}
}
A、LogTo 方法配置一个委托,接收一个字符串对象,它就是日志消息。这是配置日志输出最简单的用法;如果不配置应用程序就不会收到日志呗。
B、重写 OnModelCreating 方法,构建实体模型。Person 实体在映射时具有以下特征:
1、数据表名为 tb_persons;
2、主键的名称为 PK_Person;
3、表示姓和名的字段设置长度限制;
4、Contact 属性配置为复合属性。注意,EF Core 的"预置"约定是发现不了复合属性的,得手动去配置。约定会识别为导航属性,但 ContactInfo 类没有被配置为实体,运行后会发生异常。
有大伙伴可能不解了:FK,你这 Context 类不对劲,没有公开 DbSet<> 类型的属性,能访问实体吗?EF 能找到实体吗?
1、公开 DbSet<> 的属性是为了让预置约定自动添加实体集合,但,我不公开也不影响使用,只要调用 Set 方法,DbContext 也会添加实体集合的;
2、DbSet<> 实际上是提供一个窗口让我们执行查询,它不负责构建模型。真正用来建模的是你重写的 ModelBuilder,只要你调用 Entity 方法,它就知道你要把 Person 类作为实体添加到模型中了(如你所见,并未为 ContactInfo 类调用 Entity 方法,表明它不是实体类)。
注:这个项目要引用 Microsoft.EntityFrameworkCore.SqlServer 包,我用的是 SQL Server 数据库(LocalDB),你也可以用其他数据库,就要引用对应的包。操作方法请参考巴巴尔星人的博客。
前面提了,EF 的约定不会发现复合属性,所以你要调用 ComplexProperty 方法明确告诉 EF 这是个复合属性。
为了方便演示,本示例每次运行都会先删除数据库,再创建一个新------每次都是全新的。
using (var ctx = new MyContext())
{
// 确定数据库每次都是新的
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
// 如果空的,添加点料
DbSet<Person> persons = ctx.Set<Person>();
if (persons.Any() == false)
{
persons.Add(new Person
{
FirstName = "洪",
LastName = "水",
RegistDate = new DateOnly(2024, 2, 10),
Contact = new ContactInfo
{
Phone = "18808828811",
Email = "axax@ktv.edu.cn",
City = "天津市",
Town = "黄牛镇",
Street = "番茄路狗头街333号"
}
});
persons.Add(new Person
{
FirstName = "高",
LastName = "大桂",
RegistDate = new DateOnly(2023, 8, 25),
Contact = new()
{
Phone = "18925531100",
Email = "GreenMan123@tbcg.net",
City = "成都市",
Town = "金龙镇",
Street = "迷路1008号"
}
});
// 更新数据库
ctx.SaveChanges();
}
}
上面代码没什么要点,如果数据库已存在,删之;若不存在,建之。新数据库是没数据的,所以插入些新记录。
由于咱们在配置上下文时已经开启了日志功能,运行后咱们看看 EF 是如何对待复合属性的(主要看生成的 SQL 语句)。
CREATE TABLE [tb_persons] (
[ID] uniqueidentifier NOT NULL,
[FirstName] nvarchar(10) NOT NULL,
[LastName] nvarchar(25) NOT NULL,
[RegistDate] date NULL,
[Contact_City] nvarchar(max) NOT NULL,
[Contact_Email] nvarchar(max) NULL,
[Contact_Phone] nvarchar(max) NOT NULL,
[Contact_Street] nvarchar(max) NULL,
[Contact_Town] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Person] PRIMARY KEY ([ID])
);
这下你是否已明白,为什么复合属性的类型不需要主键了吧,因为它不会单独成表,只不过是把属性成员分散到 tb_person 表的字段列表中。所以,在数据库层面,复合对象其实是主表的一部分;而在实体层面,将其作为一个整体,我们在写.NET 代码时才有面向对象的感觉。这是为咱们程序服务的。
我们还得关心一个问题:复合属性是否支持更改跟踪------能不能自动生成更新的 SQL。咱们试一下修改其中一条记录的复合属性所引用的 ContactInfo 对象的属性值。
using (var c = new MyContext())
{
// 修改
Person? firstOne = c.Set<Person>().FirstOrDefault();
if (firstOne != null)
{
firstOne.Contact.Town = "石头镇";
// 只输出了跟踪,并未真正提交
Console.WriteLine(c.ChangeTracker.ToDebugString());
}
}
上述代码把第一条记录的联系人信息更新了,但没有调用 SaveChanges 方法,ToDebugString 方法可以输出哪些更改被跟踪到了。也就是说,这次修改实际上不会更新到数据库的。输出内容如下:
Person {ID: f9a372f8-849b-420b-8cb3-08ddd7ec9422} Unchanged
ID: 'f9a372f8-849b-420b-8cb3-08ddd7ec9422' PK
FirstName: '洪'
LastName: '水'
RegistDate: '2024/2/10'
Contact (Complex: ContactInfo)
City: '天津市'
Email: 'axax@ktv.edu.cn'
Phone: '18808828811'
Street: '番茄路狗头街333号'
Town: '石头镇' Originally '黄牛镇'
它已经跟踪到了 Town 属性的变更。
现在,咱们把更新正式提交到数据库,在更新后从数据库重新查询一下。
using (var c = new MyContext())
{
Console.WriteLine("修改前:");
PrintInfo(c.Set<Person>().AsEnumerable());
// 修改
Person? firstOne = c.Set<Person>().FirstOrDefault();
if (firstOne != null)
{
firstOne.Contact.Town = "石头镇";
// 提交更新
c.SaveChanges();
// Console.WriteLine(c.ChangeTracker.ToDebugString());
}
}
using (var c = new MyContext())
{
Console.WriteLine("\n修改后:");
PrintInfo(c.Set<Person>().AsEnumerable());
}
// 此方法用来打印实体信息
static void PrintInfo(IEnumerable<Person> persons)
{
foreach (var p in persons)
{
Console.Write(" 姓{0}名{1},来自{2}", p.FirstName, p.LastName, p.Contact.City + p.Contact.Town + p.Contact.Street);
Console.Write("\n");
}
}
更新前后各打印一次,效果如下:
修改前:
姓洪名水,来自天津市黄牛镇番茄路狗头街333号
姓高名大桂,来自成都市金龙镇迷路1008号
修改后:
姓洪名水,来自天津市石头镇番茄路狗头街333号
姓高名大桂,来自成都市金龙镇迷路1008号
接下来,老周再介绍一种特殊情况,目前来说是不能用复合属性处理的。先介绍一下实体关系:
有一个 Album 实体,它表示一张 CD 专辑。其中,它有个 Tracks 属性表示本专辑包含的曲目,这是个集合,其中每个元素是 Track 对象;
Track 实体表示单首曲子的信息,其中,它有个 Artist 属性,表示表演/演唱这首曲子的人(歌手),该属性的类型是 Performer 类。
总结一下就是:Album 类的Tracks 属性中包含 Track 类,而 Track 类的 Artist 属性引用了 Performer 类。
实体定义如下:
/// <summary>
/// 歌手信息
/// </summary>
public classPerformer
{
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 别名
/// </summary>
public string? Alias { get; set; }
/// <summary>
/// 性别
/// </summary>
public string? Gender { get; set; }
/// <summary>
/// 艺名
/// </summary>
public string? StageName { get; set; }
/// <summary>
/// 年龄
/// </summary>
public int? Age { get; set; }
/// <summary>
/// 生日
/// </summary>
public DateOnly? Birthday { get; set; }
/// <summary>
/// 简述
/// </summary>
public string? Brief { get; set; }
}
/// <summary>
/// 音轨
/// </summary>
public classTrack
{
/// <summary>
/// 轨道ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 编号
/// </summary>
public int TrackIndex { get; set; }
/// <summary>
/// 标题
/// </summary>
public string TrackName { get; set; } = string.Empty;
/// <summary>
/// 歌手
/// </summary>
public Performer Artist { get; set; } = new();
/// <summary>
/// 时长
/// </summary>
public long? Duration { get; set; }
/// <summary>
/// 注释
/// </summary>
public string? Comment { get; set; }
}
/// <summary>
/// 专辑信息
/// </summary>
public class Album
{
/// <summary>
/// 专辑ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 专辑标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 发行年份
/// </summary>
public int Year { get; set; }
/// <summary>
/// 专辑风格
/// </summary>
public string? Style { get; set; }
/// <summary>
/// 曲子数量
/// </summary>
public int TrackNum { get; set; }
/// <summary>
/// 曲目
/// </summary>
public List<Track> Tracks { get; set; } = new();
}
如果你不想给歌手/表演者实体独立建表,那此情况可以使用"从属实体"解决,即 Album 管着 Track,Track 管着 Performer。
public class TestContext : DbContext
{
// 构造函数
public TestContext(DbContextOptions<TestContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Album>(ae =>
{
ae.HasKey(a => a.ID);
// 第一层:Album --> Track,一管多
ae.OwnsMany(a => a.Tracks, obuilder =>
{
// 第二层:Track --> Performer,一管一
obuilder.OwnsOne(x => x.Artist);
});
});
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove<ForeignKeyIndexConvention>();
}
public DbSet<Album> Albums { get; set; }
}
configurationBuilder.Conventions.Remove<ForeignKeyIndexConvention>() 是把 ForeignKeyIndexConvention 约定删除,因为 Album 与 Track 之间会产生外键,而这个约定默认为外键创建索引。把这个约定从预置列表中删掉,表明不会自动创建索引。
然后咱们看一下 EF 是如何创建表的。
// 配置选项
var ctxOptions = new DbContextOptionsBuilder<TestContext>()
.UseSqlServer("server=(localdb)\\MSSQLLOCALDB;
database=db_songs")
.LogTo(msg => Console.WriteLine(msg))
.Options;
// 先初始化数据库
using (var context = new TestContext(ctxOptions))
{
// 确定数据库已创建
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
// 如果空的,放点东西进去
if (!context.Albums.Any())
{
context.Albums.Add(new Album
{
Title = "神曲集",
Year = 2025,
TrackNum = 2,
Style = "魔幻",
Tracks = [
new Track{
TrackIndex = 1,
TrackName = "我是初音未来",
Duration = 5556329L,
Artist = new Performer{
Name = "洛天依",
Alias = "ルオ・テンイ",
Age = 15,
Birthday = new DateOnly(2012, 7, 12),
Gender = "动态性别",
StageName = "洛天依",
Brief = "我不吸熊猫烧香"
}
},
new Track{
TrackIndex= 2,
TrackName = "我是洛天依",
Duration = 652224L,
Artist = new Performer{
Name = "初音未来",
Age = 16,
Alias = "初音ミク",
Birthday = new DateOnly(2007, 8, 31),
Gender = "动态性别",
StageName = "天下第一的fufu殿下",
Brief = "山东省最具代表性农作物"
}
}
]
});
context.SaveChanges();
}
}
// 筛选数据
using (var ctx = new TestContext(ctxOptions))
{
var q = from a in ctx.Albums
select new
{
a.Year,
a.Title,
Tracks = (from k in a.Tracks select new { k.
TrackIndex, k.TrackName, Artist = k.Artist.
Name })
};
foreach(var obj in q)
{
Console.WriteLine($"{obj.Year} - {obj.Title}");
foreach(var t in obj.Tracks)
{
Console.WriteLine($"\t{t.TrackIndex}. {t.TrackName}\t
演唱: {t.Artist}");
}
}
}
运行程序,看到创建表的 SQL 语句:
CREATE TABLE [Albums] (
[ID] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Year] int NOT NULL,
[Style] nvarchar(max) NULL,
[TrackNum] int NOT NULL,
CONSTRAINT [PK_Albums] PRIMARY KEY ([ID])
);
CREATE TABLE [Track] (
[ID] int NOT NULL IDENTITY,
[AlbumID] int NOT NULL,
[TrackIndex] int NOT NULL,
[TrackName] nvarchar(max) NOT NULL,
[Artist_Name] nvarchar(max) NOT NULL,
[Artist_Alias] nvarchar(max) NULL,
[Artist_Gender] nvarchar(max) NULL,
[Artist_StageName] nvarchar(max) NULL,
[Artist_Age] int NULL,
[Artist_Birthday] date NULL,
[Artist_Brief] nvarchar(max) NULL,
[Duration] bigint NULL,
[Comment] nvarchar(max) NULL,
CONSTRAINT [PK_Track] PRIMARY KEY ([AlbumID], [ID]),
CONSTRAINT [FK_Track_Albums_AlbumID] FOREIGN KEY ([AlbumID]) REFERENCES [Albums] ([ID]) ON DELETE CASCADE
);
由于一个 Album 有多首曲子(Track),所以 Track 需要建立表,而 Track 只引用一个 Performer 实例,故,Track 的 Artist 属性的处理就跟复合属性很像了。Performer 类没有单独建表,而是把它的属性成员分散在 Track 表中。
注意 Track 表是有两个主键的:曲目自己的 ID 和专辑 ID,其中,AlbumID 是外键,引用专辑表中的记录。Track 表可能放着来自同一专辑的曲目,两个主键可以更好地标识。
看看两个表中的记录。


但是,如果你坚持 Track 和 Performer 不要建表,那有没有办法解决呢?这个嘛,还真有,可以考虑只建 Album 表,把 Track 和 Performer 变成 JSON 列。其实就是将其化作 JSON,如同文本一样存到一个列中。
看配置代码:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Album>(eb =>
{
eb.OwnsMany(x => x.Tracks, ob =>
{
ob.ToJson();
ob.OwnsOne(d => d.Artist);
});
});
}
这个 ToJson 方法只要在根对象上调用一次即可,不用每层对象都调用。比如,咱们这里是让 Tracks 属性存储一个 Track 集合(列表),所以先用 OwnsMany 确定 Album 实体与 Track 对象的主从关系,然后内层操作都用 OwnedNavigationBuilder 类来完成,这时,JSON 对象的顶层(根对象)就是这个 Track 集合,故在 ob 实例上调用一次 ToJson 方法就行,之后不管 Track 类嵌套了多少对象,都自动成为 JSON 对象的一部分(本例中, Performer 对象会自动被处理)。
再次运行程序,这时 Tracks 列只是普通的文本列。
CREATE TABLE [Albums] (
[ID] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Year] int NOT NULL,
[Style] nvarchar(max) NULL,
[TrackNum] int NOT NULL,
[Tracks] nvarchar(max) NULL,
CONSTRAINT [PK_Albums] PRIMARY KEY ([ID])
);
然后查询一下这个表,看看 Tracks 字段里面存了啥东东。
[
{
"Comment": null,
"Duration": 5556329,
"ID": 0,
"TrackIndex": 1,
"TrackName": "\u6211\u662F\u521D\u97F3\u672A\u6765",
"Artist": {
"Age": 15,
"Alias": "\u30EB\u30AA\u30FB\u30C6\u30F3\u30A4",
"Birthday": "2012-07-12",
"Brief": "\u6211\u4E0D\u5438\u718A\u732B\u70E7\u9999",
"Gender": "\u52A8\u6001\u6027\u522B",
"Name": "\u6D1B\u5929\u4F9D",
"StageName": "\u6D1B\u5929\u4F9D"
}
},
{
"Comment": null,
"Duration": 652224,
"ID": 0,
"TrackIndex": 2,
"TrackName": "\u6211\u662F\u6D1B\u5929\u4F9D",
"Artist": {
"Age": 16,
"Alias": "\u521D\u97F3\u30DF\u30AF",
"Birthday": "2007-08-31",
"Brief": "\u5C71\u4E1C\u7701\u6700\u5177\u4EE3\u8868\u6027\u519C\u4F5C\u7269",
"Gender": "\u52A8\u6001\u6027\u522B",
"Name": "\u521D\u97F3\u672A\u6765",
"StageName": "\u5929\u4E0B\u7B2C\u4E00\u7684fufu\u6BBF\u4E0B"
}
}
]
现在因为 Track 类是存到 JSON 中,而不映射到数据表,所以 ID 属性不再作为主键,自然就不存在自增长了,所以,两条记录中,ID 都是 0。所以,在 Track 类定义时可以去掉 ID 属性。
public class Track
{
/// <summary>
/// 编号
/// </summary>
public int TrackIndex { get; set; }
/// <summary>
/// 标题
/// </summary>
public string TrackName { get; set; } = string.Empty;
/// <summary>
/// 歌手
/// </summary>
public Performer Artist { get; set; } = new();
/// <summary>
/// 时长
/// </summary>
public long? Duration { get; set; }
/// <summary>
/// 注释
/// </summary>
public string? Comment { get; set; }
}
再运行一遍应用程序,生成的 JSON 中就没有 ID 字段了。
[
{
"Comment": null,
"Duration": 5556329,
"TrackIndex": 1,
"TrackName": "\u6211\u662F\u521D\u97F3\u672A\u6765",
"Artist": {
"Age": 15,
"Alias": "\u30EB\u30AA\u30FB\u30C6\u30F3\u30A4",
"Birthday": "2012-07-12",
"Brief": "\u6211\u4E0D\u5438\u718A\u732B\u70E7\u9999",
"Gender": "\u52A8\u6001\u6027\u522B",
"Name": "\u6D1B\u5929\u4F9D",
"StageName": "\u6D1B\u5929\u4F9D"
}
},
{
"Comment": null,
"Duration": 652224,
"TrackIndex": 2,
"TrackName": "\u6211\u662F\u6D1B\u5929\u4F9D",
"Artist": {
"Age": 16,
"Alias": "\u521D\u97F3\u30DF\u30AF",
"Birthday": "2007-08-31",
"Brief": "\u5C71\u4E1C\u7701\u6700\u5177\u4EE3\u8868\u6027\u519C\u4F5C\u7269",
"Gender": "\u52A8\u6001\u6027\u522B",
"Name": "\u521D\u97F3\u672A\u6765",
"StageName": "\u5929\u4E0B\u7B2C\u4E00\u7684fufu\u6BBF\u4E0B"
}
}
]
JSON 列的字段名默认与属性名(Tracks)相同,你如果想用其他名字,可以在调用 ToJson 方法时指定。
modelBuilder.Entity<Album>(eb =>
{
eb.OwnsMany(x => x.Tracks, ob =>
{
ob.ToJson("track_list");
ob.OwnsOne(d => d.Artist);
});
});
在创建数据表的时候,列名就会自动变更。
CREATE TABLE [Albums] (
[ID] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Year] int NOT NULL,
[Style] nvarchar(max) NULL,
[TrackNum] int NOT NULL,
[track_list] nvarchar(max) NULL,
CONSTRAINT [PK_Albums] PRIMARY KEY ([ID])
);

===============================================================================================
好了,正题今天就水到这儿了,下面是老周讲故事时间。
曾在微博上收到一条私信说:老周,我感觉你很厉害,好像啥都研究得那么透彻。真佩服你!
这里要纠正一下,老周并不是啥都能研究得很透彻,比如人心,老周就无法研究。其实,老周只不过是这儿会一点,那儿会一点,啥都会,啥都不精。如此而已。
大伙伴们不用佩服老周的,只要你能确定:你的智商和脑子是正常的,那,你也能做到像老周这样的。老周从小就养成一个毛病,爱钻牛角尖。许多事情,仅仅知道是啥是不满足的,就很好奇想要弄清楚为什么。所以在学习一个东西,老周习惯性地把重点放在原理上,为什么会这样,为什么要这样做......老周一向比较自律,能静下心来做自己想做的事。也许,从小练书法确实是有好处的,人不会太急躁。尽管人的精力是有限的,不可能把世上的事情都弄明白。可是,能弄明白一件算一件,好歹对得起自己。如果自己花了心思去学习一个东西,连那是啥玩意儿都搞不懂,那岂不是浪费大好光阴吗?从小老师就教我们(不知道现在的老师还教不教),一寸光阴一寸金,下一句就不必说了吧。所以你看到老周写的文章,很喜欢告诉大伙原理性的东西,哪怕老周写的东西可能有错。至少这也是我的感悟,有错误的话你可以发个私信告诉我的。
现在有些人被别有用心的人误导,连放个P都追求快,快,快,赶着投胎似的。实际上,你可否想过,越是浮躁,你最终的损失越大。同样是花了时间和精力在某件事情上,你这搞搞那搞搞,最后连跟毛都搞不清楚,而别人是一件一件的梳理得一清二楚三通透。到最后,你觉得你比别人跑得更远了吗?
所以啊,有些事情是需要一个过程,该走的路就得走,再急也是徒增烦恼。小时候一直不明白一句话"烦恼即菩提",这烦恼和菩提树是八竿子打不着的,毫无关系。长大后就懂了,就是毫无关系啊,烦恼就是你给自己找苦吃,把毫无关系的事情硬是往自己的情绪上塞。空想往往不会有收获,但抱着试一试的心态行动了,说不定就有所感悟了。