home commit with new plugins
This commit is contained in:
Vendored
+3
-1
@@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"theme": "obsidian"
|
||||||
|
}
|
||||||
Vendored
+5
@@ -9,6 +9,11 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"ctime": 1775896004734,
|
"ctime": 1775896004734,
|
||||||
"path": "笔记/Unity笔记汇总.md"
|
"path": "笔记/Unity笔记汇总.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"ctime": 1781959857783,
|
||||||
|
"path": "主页.md"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"obsidian-day-planner",
|
||||||
|
"obsidian-tasks-plugin",
|
||||||
|
"easy-tracker"
|
||||||
|
]
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"folder": "日记",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"template": "模板文件夹/每日计划.md"
|
||||||
|
}
|
||||||
+16
File diff suppressed because one or more lines are too long
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "easy-tracker",
|
||||||
|
"name": "Easy Tracker",
|
||||||
|
"version": "1.0.6",
|
||||||
|
"minAppVersion": "1.8.0",
|
||||||
|
"description": "Instantly track goals and habits. Simple, beautiful, and configuration-free.",
|
||||||
|
"author": "Hunter Ji",
|
||||||
|
"authorUrl": "https://hunterji.com",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
+245
@@ -0,0 +1,245 @@
|
|||||||
|
.easy-tracker-button-group-container {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.easy-tracker-button-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.easy-tracker-button-group .btn {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 12px 16px;
|
||||||
|
min-height: 44px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--interactive-hover);
|
||||||
|
color: var(--text-normal);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.easy-tracker-button-group .btn:hover,
|
||||||
|
.easy-tracker-button-group .btn:active {
|
||||||
|
background: var(--background-modifier-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.easy-tracker-button-group {
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.easy-tracker-button-group .btn {
|
||||||
|
padding: 14px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__item {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--background-primary);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__value--positive {
|
||||||
|
color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__value--warning {
|
||||||
|
color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-daily-overview__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.easy-tracker-daily-overview {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.easy-tracker-daily-overview__grid {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.easy-tracker-daily-overview__item {
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 88px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-card {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-normal);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-card-message,
|
||||||
|
.easy-tracker-my-goal {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--background-primary);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
/* Calendar Heatmap */
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-root {
|
||||||
|
font-family: var(--font-interface);
|
||||||
|
--easy-tracker-heatmap-size: 14px;
|
||||||
|
--easy-tracker-heatmap-gap: 2px;
|
||||||
|
|
||||||
|
/* Default Colors (Light) */
|
||||||
|
--easy-tracker-heatmap-color-level-0: #ebedf0;
|
||||||
|
--easy-tracker-heatmap-color-level-1: #9be9a8;
|
||||||
|
--easy-tracker-heatmap-color-level-2: #40c463;
|
||||||
|
--easy-tracker-heatmap-color-level-3: #30a14e;
|
||||||
|
--easy-tracker-heatmap-color-level-4: #216e39;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .easy-tracker-heatmap-root {
|
||||||
|
/* Default Colors (Dark) */
|
||||||
|
--easy-tracker-heatmap-color-level-0: #161b22;
|
||||||
|
--easy-tracker-heatmap-color-level-1: #0e4429;
|
||||||
|
--easy-tracker-heatmap-color-level-2: #006d32;
|
||||||
|
--easy-tracker-heatmap-color-level-3: #26a641;
|
||||||
|
--easy-tracker-heatmap-color-level-4: #39d353;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: max-content;
|
||||||
|
column-gap: var(--easy-tracker-heatmap-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-week {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(7, 1fr);
|
||||||
|
row-gap: var(--easy-tracker-heatmap-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-day {
|
||||||
|
width: var(--easy-tracker-heatmap-size);
|
||||||
|
height: var(--easy-tracker-heatmap-size);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--easy-tracker-heatmap-color-level-0);
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(27, 31, 35, 0.06);
|
||||||
|
}
|
||||||
|
.theme-dark .easy-tracker-heatmap-day {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-day[data-level="0"] { background-color: var(--easy-tracker-heatmap-color-level-0); }
|
||||||
|
.easy-tracker-heatmap-day[data-level="1"] { background-color: var(--easy-tracker-heatmap-color-level-1); }
|
||||||
|
.easy-tracker-heatmap-day[data-level="2"] { background-color: var(--easy-tracker-heatmap-color-level-2); }
|
||||||
|
.easy-tracker-heatmap-day[data-level="3"] { background-color: var(--easy-tracker-heatmap-color-level-3); }
|
||||||
|
.easy-tracker-heatmap-day[data-level="4"] { background-color: var(--easy-tracker-heatmap-color-level-4); }
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--background-modifier-message);
|
||||||
|
color: var(--text-normal);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-legend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-legend .easy-tracker-heatmap-swatch {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--easy-tracker-heatmap-size);
|
||||||
|
height: var(--easy-tracker-heatmap-size);
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--easy-tracker-heatmap-color-level-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-legend .easy-tracker-heatmap-swatch[data-level="0"] { background-color: var(--easy-tracker-heatmap-color-level-0); }
|
||||||
|
.easy-tracker-heatmap-legend .easy-tracker-heatmap-swatch[data-level="1"] { background-color: var(--easy-tracker-heatmap-color-level-1); }
|
||||||
|
.easy-tracker-heatmap-legend .easy-tracker-heatmap-swatch[data-level="2"] { background-color: var(--easy-tracker-heatmap-color-level-2); }
|
||||||
|
.easy-tracker-heatmap-legend .easy-tracker-heatmap-swatch[data-level="3"] { background-color: var(--easy-tracker-heatmap-color-level-3); }
|
||||||
|
.easy-tracker-heatmap-legend .easy-tracker-heatmap-swatch[data-level="4"] { background-color: var(--easy-tracker-heatmap-color-level-4); }
|
||||||
|
|
||||||
|
.easy-tracker-heatmap-labels {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(7, var(--easy-tracker-heatmap-size));
|
||||||
|
row-gap: var(--easy-tracker-heatmap-gap);
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: var(--easy-tracker-heatmap-size);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"snapStepMinutes": 10,
|
||||||
|
"progressIndicator": "mini-timeline",
|
||||||
|
"showTaskNotification": true,
|
||||||
|
"zoomLevel": 2,
|
||||||
|
"timelineIcon": "calendar-with-checkmark",
|
||||||
|
"endLabel": "All done",
|
||||||
|
"startHour": 6,
|
||||||
|
"timelineDateFormat": "YYYY-MM-DD",
|
||||||
|
"centerNeedle": false,
|
||||||
|
"plannerHeading": "Day planner",
|
||||||
|
"plannerHeadingLevel": 1,
|
||||||
|
"timelineColored": false,
|
||||||
|
"timelineStartColor": "#006466",
|
||||||
|
"timelineEndColor": "#4d194d",
|
||||||
|
"timestampFormat": "HH:mm",
|
||||||
|
"hourFormat": "H",
|
||||||
|
"extendDurationUntilNext": false,
|
||||||
|
"defaultDurationMinutes": 30,
|
||||||
|
"minimalDurationMinutes": 10,
|
||||||
|
"showTimestampInTaskBlock": false,
|
||||||
|
"showUncheduledTasks": true,
|
||||||
|
"showUnscheduledNestedTasks": true,
|
||||||
|
"showNow": true,
|
||||||
|
"showNext": true,
|
||||||
|
"pluginVersion": "0.30.0",
|
||||||
|
"showCompletedTasks": true,
|
||||||
|
"showSubtasksInTaskBlocks": true,
|
||||||
|
"icals": [],
|
||||||
|
"colorOverrides": [],
|
||||||
|
"releaseNotes": true,
|
||||||
|
"taskStatusOnCreation": " ",
|
||||||
|
"eventFormatOnCreation": "task",
|
||||||
|
"sortTasksInPlanAfterEdit": false,
|
||||||
|
"firstDayOfWeek": "monday",
|
||||||
|
"multiDayRange": "3-days",
|
||||||
|
"showTimelineInSidebar": true,
|
||||||
|
"timelineColumns": {
|
||||||
|
"planner": true,
|
||||||
|
"timeTracker": false
|
||||||
|
}
|
||||||
|
}
|
||||||
+1689
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "obsidian-day-planner",
|
||||||
|
"name": "Day Planner",
|
||||||
|
"version": "0.30.0",
|
||||||
|
"minAppVersion": "0.16.0",
|
||||||
|
"description": "A day planner with clean UI and readable syntax",
|
||||||
|
"author": "James Lynch, continued by Ivan Lednev",
|
||||||
|
"authorUrl": "https://github.com/ivan-lednev",
|
||||||
|
"fundingUrl": "https://www.buymeacoffee.com/machineelf",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
+153
File diff suppressed because one or more lines are too long
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "obsidian-kanban",
|
||||||
|
"name": "Kanban",
|
||||||
|
"version": "2.0.51",
|
||||||
|
"minAppVersion": "1.0.0",
|
||||||
|
"description": "Create markdown-backed Kanban boards in Obsidian.",
|
||||||
|
"author": "mgmeyers",
|
||||||
|
"authorUrl": "https://github.com/mgmeyers/obsidian-kanban",
|
||||||
|
"helpUrl": "https://publish.obsidian.md/kanban/Obsidian+Kanban+Plugin",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
+105
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"presets": {
|
||||||
|
"this_file": "path includes {{query.file.path}}",
|
||||||
|
"this_folder": "folder includes {{query.file.folder}}",
|
||||||
|
"this_folder_only": "filter by function task.file.folder === query.file.folder",
|
||||||
|
"this_root": "root includes {{query.file.root}}",
|
||||||
|
"hide_date_fields": "# Hide any values for all date fields\nhide due date\nhide scheduled date\nhide start date\nhide created date\nhide done date\nhide cancelled date",
|
||||||
|
"hide_non_date_fields": "# Hide all the non-date fields, but not tags\nhide id\nhide depends on\nhide recurrence rule\nhide on completion\nhide priority",
|
||||||
|
"hide_query_elements": "# Hide toolbar, postpone, edit and backlinks\nhide toolbar\nhide postpone button\nhide edit button\nhide backlinks",
|
||||||
|
"hide_everything": "# Hide everything except description and any tags\npreset hide_date_fields\npreset hide_non_date_fields\npreset hide_query_elements"
|
||||||
|
},
|
||||||
|
"globalQuery": "",
|
||||||
|
"globalFilter": "",
|
||||||
|
"removeGlobalFilter": false,
|
||||||
|
"taskFormat": "tasksPluginEmoji",
|
||||||
|
"setCreatedDate": false,
|
||||||
|
"setDoneDate": true,
|
||||||
|
"setCancelledDate": true,
|
||||||
|
"autoSuggestInEditor": true,
|
||||||
|
"autoSuggestMinMatch": 0,
|
||||||
|
"autoSuggestMaxItems": 20,
|
||||||
|
"provideAccessKeys": true,
|
||||||
|
"useFilenameAsScheduledDate": false,
|
||||||
|
"filenameAsScheduledDateFormat": "",
|
||||||
|
"filenameAsDateFolders": [],
|
||||||
|
"recurrenceOnNextLine": false,
|
||||||
|
"removeScheduledDateOnRecurrence": false,
|
||||||
|
"searchResults": {
|
||||||
|
"taskCountLocation": "bottom"
|
||||||
|
},
|
||||||
|
"statusSettings": {
|
||||||
|
"coreStatuses": [
|
||||||
|
{
|
||||||
|
"symbol": " ",
|
||||||
|
"name": "Todo",
|
||||||
|
"nextStatusSymbol": "x",
|
||||||
|
"availableAsCommand": true,
|
||||||
|
"type": "TODO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "x",
|
||||||
|
"name": "Done",
|
||||||
|
"nextStatusSymbol": " ",
|
||||||
|
"availableAsCommand": true,
|
||||||
|
"type": "DONE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customStatuses": [
|
||||||
|
{
|
||||||
|
"symbol": "/",
|
||||||
|
"name": "In Progress",
|
||||||
|
"nextStatusSymbol": "x",
|
||||||
|
"availableAsCommand": true,
|
||||||
|
"type": "IN_PROGRESS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "-",
|
||||||
|
"name": "Cancelled",
|
||||||
|
"nextStatusSymbol": " ",
|
||||||
|
"availableAsCommand": true,
|
||||||
|
"type": "CANCELLED"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isShownInEditModal": {
|
||||||
|
"priority": true,
|
||||||
|
"recurrence": true,
|
||||||
|
"due": true,
|
||||||
|
"scheduled": true,
|
||||||
|
"start": true,
|
||||||
|
"before_this": true,
|
||||||
|
"after_this": true,
|
||||||
|
"status": true,
|
||||||
|
"created": true,
|
||||||
|
"done": true,
|
||||||
|
"cancelled": true
|
||||||
|
},
|
||||||
|
"dismissedNotices": {
|
||||||
|
"live-preview-callout-warning": false
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"INTERNAL_TESTING_ENABLED_BY_DEFAULT": true
|
||||||
|
},
|
||||||
|
"generalSettings": {},
|
||||||
|
"headingOpened": {
|
||||||
|
"核心状态": true,
|
||||||
|
"自定义状态": true
|
||||||
|
},
|
||||||
|
"debugSettings": {
|
||||||
|
"ignoreSortInstructions": false,
|
||||||
|
"showTaskHiddenData": false,
|
||||||
|
"recordTimings": false
|
||||||
|
},
|
||||||
|
"loggingOptions": {
|
||||||
|
"minLevels": {
|
||||||
|
"": "info",
|
||||||
|
"tasks": "info",
|
||||||
|
"tasks.Cache": "info",
|
||||||
|
"tasks.Events": "info",
|
||||||
|
"tasks.File": "info",
|
||||||
|
"tasks.Query": "info",
|
||||||
|
"tasks.Task": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+531
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "obsidian-tasks-plugin",
|
||||||
|
"name": "Tasks",
|
||||||
|
"version": "8.2.1",
|
||||||
|
"minAppVersion": "1.8.7",
|
||||||
|
"description": "Track tasks across your vault. Supports due dates, recurring tasks, done dates, sub-set of checklist items, and filtering.",
|
||||||
|
"helpUrl": "https://publish.obsidian.md/tasks/",
|
||||||
|
"author": "Clare Macrae and Ilyas Landikov (created by Martin Schenck)",
|
||||||
|
"authorUrl": "https://github.com/obsidian-tasks-group",
|
||||||
|
"fundingUrl": "https://github.com/sponsors/claremacrae",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+23
-1
@@ -3,6 +3,28 @@
|
|||||||
"aliases": "aliases",
|
"aliases": "aliases",
|
||||||
"cssclasses": "multitext",
|
"cssclasses": "multitext",
|
||||||
"tags": "tags",
|
"tags": "tags",
|
||||||
"date": "date"
|
"date": "date",
|
||||||
|
"TQ_explain": "checkbox",
|
||||||
|
"TQ_extra_instructions": "text",
|
||||||
|
"TQ_short_mode": "checkbox",
|
||||||
|
"TQ_show_backlink": "checkbox",
|
||||||
|
"TQ_show_cancelled_date": "checkbox",
|
||||||
|
"TQ_show_created_date": "checkbox",
|
||||||
|
"TQ_show_depends_on": "checkbox",
|
||||||
|
"TQ_show_done_date": "checkbox",
|
||||||
|
"TQ_show_due_date": "checkbox",
|
||||||
|
"TQ_show_edit_button": "checkbox",
|
||||||
|
"TQ_show_id": "checkbox",
|
||||||
|
"TQ_show_on_completion": "checkbox",
|
||||||
|
"TQ_show_postpone_button": "checkbox",
|
||||||
|
"TQ_show_priority": "checkbox",
|
||||||
|
"TQ_show_recurrence_rule": "checkbox",
|
||||||
|
"TQ_show_scheduled_date": "checkbox",
|
||||||
|
"TQ_show_start_date": "checkbox",
|
||||||
|
"TQ_show_tags": "checkbox",
|
||||||
|
"TQ_show_task_count": "checkbox",
|
||||||
|
"TQ_show_toolbar": "checkbox",
|
||||||
|
"TQ_show_tree": "checkbox",
|
||||||
|
"TQ_show_urgency": "checkbox"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "a693de761d542948",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "de35c53ad452014d",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "e7c250592202ef1d",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "待办/Unity重学.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "Unity重学"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "630f8165e6a24a86",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "f0756957054a6fce",
|
||||||
|
"type": "tabs",
|
||||||
|
"dimension": 79.77755308392315,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "ebc68222154201be",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "byModifiedTime",
|
||||||
|
"autoReveal": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-folder-closed",
|
||||||
|
"title": "文件列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "991397493df067e8",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-bookmark",
|
||||||
|
"title": "书签"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cc621346ccdf86c9",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {
|
||||||
|
"file": "笔记/Unity笔记汇总.md",
|
||||||
|
"followCursor": false,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-list",
|
||||||
|
"title": "Unity笔记汇总 的大纲"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f8fdc77a59b98c85",
|
||||||
|
"type": "tabs",
|
||||||
|
"dimension": 20.222446916076844,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "ecccd9af8286ea29",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "[\"tags\"]",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-search",
|
||||||
|
"title": "搜索"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "c7920e2ce907397e",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "a5f3c68213ee3916",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "42aa94a29399c26f",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"file": "笔记/Unity笔记汇总.md",
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-coming-in",
|
||||||
|
"title": "Unity笔记汇总 的反向链接列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d651b27dcd912ace",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"file": "笔记/CSharp/核心/索引器.md",
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": false
|
||||||
|
},
|
||||||
|
"icon": "links-going-out",
|
||||||
|
"title": "索引器 的出链列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b395181a320df62d",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-tags",
|
||||||
|
"title": "标签"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d9c193444d1f152f",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "all-properties",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-archive",
|
||||||
|
"title": "添加笔记属性"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"currentTab": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
"left-ribbon": {
|
||||||
|
"hiddenItems": {
|
||||||
|
"switcher:打开快速切换": false,
|
||||||
|
"graph:查看关系图谱": false,
|
||||||
|
"canvas:新建白板": false,
|
||||||
|
"daily-notes:打开/创建今天的日记": false,
|
||||||
|
"templates:插入模板": false,
|
||||||
|
"command-palette:打开命令面板": false,
|
||||||
|
"bases:新建数据库": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": "e7c250592202ef1d",
|
||||||
|
"lastOpenFiles": [
|
||||||
|
"笔记/CSharp笔记汇总.md",
|
||||||
|
"待办/Unity重学.md",
|
||||||
|
"待办/CSharp重学.md",
|
||||||
|
"笔记/CSharp/进阶/Hashtable.md",
|
||||||
|
"笔记/CSharp/进阶/Stack.md",
|
||||||
|
"简历_综合应届生版.md",
|
||||||
|
"简历_网络运维技术支持方向.md",
|
||||||
|
"服务器迁移/网站容器迁移与SSL续签手册.md",
|
||||||
|
"简历_Unity游戏开发方向.md",
|
||||||
|
"密码本/Teamspeak Recovery Key.md",
|
||||||
|
"Teamspeak/网站容器迁移与SSL续签手册.md",
|
||||||
|
"笔记/CSharp/基础/值类型和引用类型.md",
|
||||||
|
"笔记/CSharp/入门/字符串拼接.md",
|
||||||
|
"待办/大待办.md",
|
||||||
|
"未命名.md",
|
||||||
|
"未命名 1.md",
|
||||||
|
"服务器迁移",
|
||||||
|
"笔记/CSharp/进阶/Queue.md",
|
||||||
|
"笔记/CSharp/进阶/ArrayList.md",
|
||||||
|
"笔记/Unity笔记汇总.md",
|
||||||
|
"笔记/CSharp/练习/CSharp综合练习-02.md",
|
||||||
|
"笔记/CSharp/进阶",
|
||||||
|
"笔记/CSharp/核心/索引器.md",
|
||||||
|
"笔记/CSharp/核心/接口.md",
|
||||||
|
"笔记/CSharp/基础/枚举.md",
|
||||||
|
"笔记/CSharp/核心/抽象类和抽象方法.md",
|
||||||
|
"笔记/CSharp/核心/密封函数.md",
|
||||||
|
"笔记/CSharp/核心/密封类.md",
|
||||||
|
"未命名.base",
|
||||||
|
"未命名.canvas",
|
||||||
|
"密码本",
|
||||||
|
"图库/CSharp/Pasted image 20260412210327.png",
|
||||||
|
"笔记/CSharp/练习",
|
||||||
|
"图库/CSharp/Pasted image 20260412210704.png",
|
||||||
|
"图库/CSharp/Pasted image 20260412210810.png",
|
||||||
|
"图库/CSharp/Pasted image 20260412210946.png",
|
||||||
|
"笔记/CSharp/杂记",
|
||||||
|
"Pasted image 20260412210946.png",
|
||||||
|
"Pasted image 20260412210810.png",
|
||||||
|
"Pasted image 20260412210704.png",
|
||||||
|
"图库/CSharp/Pasted image 20260412210516.png",
|
||||||
|
"图库/CSharp",
|
||||||
|
"图库",
|
||||||
|
"笔记/CSharp/核心",
|
||||||
|
"笔记/CSharp/基础"
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "E:\\Obsidian\\OneDrive\\Apps\\remotely-save\\MyNotes\\",
|
||||||
|
"Documents": [],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 200,
|
||||||
|
"SelectedChildIndex": -1,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:128:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:129:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "E:\\Obsidian\\OneDrive\\Apps\\remotely-save\\MyNotes\\",
|
||||||
|
"Documents": [],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 200,
|
||||||
|
"SelectedChildIndex": -1,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:129:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "C:\\Users\\Kister\\OneDrive\\Apps\\remotely-save\\MyNotes\\",
|
||||||
|
"Documents": [],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"CurrentProjectSetting": null
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ExpandedNodes": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"PreviewInSolutionExplorer": false
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,50 @@
|
|||||||
|
## 今日状态
|
||||||
|
|
||||||
|
```easy-tracker-daily-overview
|
||||||
|
```
|
||||||
|
|
||||||
|
```easy-tracker-year-calendar-heatmap
|
||||||
|
```
|
||||||
|
|
||||||
|
```easy-tracker-buttons
|
||||||
|
稍微做了一点 | 1
|
||||||
|
顺利完成 | 2
|
||||||
|
额外完成 | 3
|
||||||
|
```
|
||||||
|
|
||||||
|
```easy-tracker-my-goal
|
||||||
|
每天完成一个小闭环:计划、执行、复盘。
|
||||||
|
```
|
||||||
|
|
||||||
|
# ==今日任务==
|
||||||
|
|
||||||
|
```tasks
|
||||||
|
not done
|
||||||
|
(scheduled today) OR (due today)
|
||||||
|
sort by scheduled
|
||||||
|
sort by priority
|
||||||
|
```
|
||||||
|
|
||||||
|
## 明日任务
|
||||||
|
|
||||||
|
```tasks
|
||||||
|
not done
|
||||||
|
(due tomorrow)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 学习列表
|
||||||
|
```tasks
|
||||||
|
not done
|
||||||
|
(tags include #study/csharp) OR (tags include #study/unity)
|
||||||
|
sort by priority
|
||||||
|
group by filename
|
||||||
|
```
|
||||||
|
|
||||||
|
## 已完成
|
||||||
|
|
||||||
|
```tasks
|
||||||
|
done today
|
||||||
|
sort by done reverse
|
||||||
|
```
|
||||||
|
|
||||||
|
* 2026-06-20 - 2
|
||||||
+43
-43
@@ -1,43 +1,43 @@
|
|||||||
- [x] [[常量]]
|
- [x] [[常量]] #study/csharp
|
||||||
- [x] [[转义字符]]
|
- [x] [[转义字符]] #study/csharp
|
||||||
- [x] [[格式转换]]
|
- [x] [[格式转换]] #study/csharp
|
||||||
- [x] [[字符串拼接]]
|
- [x] [[字符串拼接]] #study/csharp
|
||||||
- [x] [[三目运算符]]
|
- [x] [[三目运算符]] #study/csharp
|
||||||
- [x] 枚举
|
- [x] 枚举 #study/csharp
|
||||||
- [x] 值和引用类型
|
- [x] 值和引用类型 #study/csharp
|
||||||
- [x] ref和out
|
- [x] ref和out #study/csharp
|
||||||
- [x] 变长参数
|
- [x] 变长参数 #study/csharp
|
||||||
- [x] 结构体和枚举
|
- [x] 结构体和枚举 #study/csharp
|
||||||
- [x] 成员变量和访问修饰符
|
- [x] 成员变量和访问修饰符 #study/csharp
|
||||||
- [x] 垃圾回收
|
- [x] 垃圾回收 #study/csharp
|
||||||
- [x] 成员属性
|
- [x] 成员属性 #study/csharp
|
||||||
- [x] 索引器
|
- [x] 索引器 #study/csharp
|
||||||
- [x] 静态成员静态类和静态构造函数
|
- [x] 静态成员静态类和静态构造函数 #study/csharp
|
||||||
- [ ] 运算符重载
|
- [x] 运算符重载 #study/csharp
|
||||||
- [x] 内部类和分部类
|
- [x] 内部类和分部类 #study/csharp
|
||||||
- [x] 继承
|
- [x] 继承 #study/csharp
|
||||||
- [ ] 装箱与拆箱
|
- [x] 装箱与拆箱 #study/csharp
|
||||||
- [ ] 密封类
|
- [x] 密封类 #study/csharp
|
||||||
- [ ] 多态vob
|
- [x] 多态vob #study/csharp
|
||||||
- [ ] 抽象类和抽象函数
|
- [x] [[抽象类和抽象方法]] #study/csharp
|
||||||
- [ ] 接口
|
- [x] [[接口]] #study/csharp
|
||||||
- [ ] 密封函数
|
- [x] [[密封函数]] #study/csharp
|
||||||
- [ ] 命名空间
|
- [x] [[命名空间]] #study/csharp
|
||||||
- [ ] 抽象类的接口的区别
|
- [x] [[抽象类和接口的区别|抽象类的接口的区别]] #study/csharp
|
||||||
- [ ] ArrayList
|
- [x] [[ArrayList]] #study/csharp
|
||||||
- [ ] Stack
|
- [x] [[Stack]] #study/csharp
|
||||||
- [ ] Queue
|
- [x] [[Queue]] #study/csharp
|
||||||
- [ ] Hashtable
|
- [x] [[Hashtable]] #study/csharp
|
||||||
- [ ] 泛型和泛型约束
|
- [ ] 泛型和泛型约束 #study/csharp
|
||||||
- [ ] List
|
- [ ] List #study/csharp
|
||||||
- [ ] Dictionary
|
- [ ] Dictionary #study/csharp
|
||||||
- [ ] LinkedList
|
- [ ] LinkedList #study/csharp
|
||||||
- [ ] 泛型栈和队列
|
- [ ] 泛型栈和队列 #study/csharp
|
||||||
- [ ] 委托事件
|
- [ ] 委托事件 #study/csharp
|
||||||
- [ ] 匿名函数
|
- [ ] 匿名函数 #study/csharp
|
||||||
- [ ] lambda
|
- [ ] lambda #study/csharp
|
||||||
- [ ] List排序
|
- [ ] List排序 #study/csharp
|
||||||
- [ ] 协变逆变
|
- [ ] 协变逆变 #study/csharp
|
||||||
- [ ] 多线程
|
- [ ] 多线程 #study/csharp
|
||||||
- [ ] 预处理器
|
- [ ] 预处理器 #study/csharp
|
||||||
- [ ] 反射
|
- [ ] 反射 #study/csharp
|
||||||
|
|||||||
+6
-4
@@ -1,4 +1,6 @@
|
|||||||
- [ ] 入门重学
|
# Unity 重学
|
||||||
- [ ] 基础学习
|
|
||||||
- [ ] 核心学习
|
- [ ] 入门重学 #study/unity
|
||||||
- [ ] 进阶学习
|
- [ ] 基础学习 #study/unity
|
||||||
|
- [ ] 核心学习 #study/unity
|
||||||
|
- [ ] 进阶学习 #study/unity
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
- [ ] PUBG任务
|
# 日常 List
|
||||||
- [ ] 设置闹钟
|
> 这里放每天会自动刷新的小任务。完成后 Tasks 会按 `🔁 every day` 生成下一次。
|
||||||
- [ ]
|
|
||||||
|
- [ ] eNSP #daily 🔺 🔁 every day 📅 2026-06-21
|
||||||
|
- [ ] Linux #daily 🔺 🔁 every day 📅 2026-06-21
|
||||||
|
- [ ] C# #daily ⏫ 🔁 every day 📅 2026-06-21
|
||||||
|
- [ ] C++ 或 UE #weekly 🔽 🔁 every week 📅 2026-06-21
|
||||||
|
- [ ] 今日复盘:记录完成情况和明天第一件事 🔼 🔁 every day 📅 2026-06-21
|
||||||
|
- [ ] 学习吉他 ⏬ 🔁 every day 📅 2026-06-21
|
||||||
|
- [x] 今日复盘:记录完成情况和明天第一件事 🔼 🔁 every day 📅 2026-06-20 ✅ 2026-06-20
|
||||||
|
|
||||||
|
## 显示今日的样例:
|
||||||
|
|
||||||
|
```tasks
|
||||||
|
not done
|
||||||
|
(scheduled today) OR (due today)
|
||||||
|
sort by scheduled
|
||||||
|
sort by priority
|
||||||
|
```
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- [ ] 优化ui
|
|
||||||
- [ ] 稳定输出
|
|
||||||
- [ ] 扩容任务
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
**Ctrl+B**
|
**Ctrl+B**
|
||||||
*Ctrl+I*
|
*Ctrl+I*
|
||||||
==Ctrl+Shift+H==
|
==Ctrl+Shift+H==
|
||||||
|
Ctrl+P命令面版
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# {{date}}
|
||||||
|
|
||||||
|
## 今日状态
|
||||||
|
|
||||||
|
- 起床状态:
|
||||||
|
- 今日主线:
|
||||||
|
|
||||||
|
# 今日任务
|
||||||
|
|
||||||
|
```tasks
|
||||||
|
not done
|
||||||
|
(scheduled today) OR (due today)
|
||||||
|
sort by scheduled
|
||||||
|
sort by priority
|
||||||
|
```
|
||||||
|
|
||||||
|
# Day planner
|
||||||
|
|
||||||
|
> ==记得把今日任务详细的规划在时间线上↓==
|
||||||
|
|
||||||
|
- [ ] 09:00 - 09:30
|
||||||
|
- [ ] 09:40 - 10:20
|
||||||
|
- [ ] 10:30 - 11:00
|
||||||
|
- [ ] 22:30 - 22:45
|
||||||
|
|
||||||
|
## 复盘
|
||||||
|
|
||||||
|
- 今天完成最多的是:
|
||||||
|
- 明天第一件事:
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
这是你的新*仓库*。
|
|
||||||
|
|
||||||
写点笔记,[[创建链接]],或者试一试[导入器](https://help.obsidian.md/Plugins/Importer)插件!
|
|
||||||
|
|
||||||
当你准备好了,就将该笔记文件删除,使这个仓库为你所用。
|
|
||||||
+20
-2
@@ -1,13 +1,31 @@
|
|||||||
### 三大特性
|
### 三大特性
|
||||||
1. 封装
|
1. 封装
|
||||||
|
把数据和操作数据的方法放在一起,并隐藏实现细节,只对外暴露合适的接口。
|
||||||
2. [[继承]]
|
2. [[继承]]
|
||||||
3. 多态
|
在已有类型的基础上复用和扩展功能,表达清晰的 `is-a` 关系。
|
||||||
|
3. [[多态vob|多态]]
|
||||||
|
同一个调用入口,在不同对象上表现出不同实现。
|
||||||
|
|
||||||
|
补充:
|
||||||
|
- 三大特性是面向对象里最常见的基础概念。
|
||||||
|
- 实际写代码时,通常是先做好封装,再视情况决定要不要用继承和多态。
|
||||||
|
|
||||||
#### 七大原则
|
#### 七大原则
|
||||||
1. 开闭原则
|
1. 开闭原则
|
||||||
|
对扩展开放,对修改关闭。
|
||||||
2. 依赖倒转原则
|
2. 依赖倒转原则
|
||||||
3. 里氏替换原则
|
高层模块不依赖低层细节,双方都依赖抽象。
|
||||||
|
3. [[里氏替换原则]]
|
||||||
|
子类对象应该能替换父类对象而不破坏程序正确性。
|
||||||
4. 单一职责原则
|
4. 单一职责原则
|
||||||
|
一个类最好只负责一类事情。
|
||||||
5. 接口隔离原则
|
5. 接口隔离原则
|
||||||
|
接口要小而专,不要逼调用方依赖自己用不到的成员。
|
||||||
6. 合成复用原则
|
6. 合成复用原则
|
||||||
|
优先使用组合,而不是为了复用就滥用继承。
|
||||||
7. 迪米特法则
|
7. 迪米特法则
|
||||||
|
对象之间尽量少了解内部细节,降低耦合。
|
||||||
|
|
||||||
|
补充:
|
||||||
|
- 这些原则是设计经验,不是语法规则。
|
||||||
|
- 新手阶段先理解“为什么这么设计会更好维护”,比死记名字更重要。
|
||||||
|
|||||||
+47
-40
@@ -1,48 +1,55 @@
|
|||||||
|
# ref 和 out
|
||||||
通常用法:
|
通常用法:
|
||||||
解决值[[值类型和引用类型]]在函数内外部互相传递时的问题
|
- 解决“函数内部改了值,函数外部也要同步变化”的需求
|
||||||
函数内部改外部传入也改
|
- 让方法可以直接操作调用方变量
|
||||||
==ref和out的区别:==
|
|
||||||
ref:
|
区别:
|
||||||
- 传入的参数必须初始化(赋值)
|
1. `ref`
|
||||||
- 方法内可读可写
|
- 传入前必须先初始化
|
||||||
out:
|
- 方法内可以读,也可以写
|
||||||
- 传入前**可以不初始化**
|
2. `out`
|
||||||
- 方法内**必须赋值**,否则编译报错
|
- 传入前可以不初始化
|
||||||
|
- 方法返回前必须赋值
|
||||||
|
- 常用于“额外带回一个结果”
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
在函数传参时,两边都要加上修饰词
|
定义和调用两边都要写关键字。
|
||||||
ref:
|
|
||||||
```Csharp
|
`ref`:
|
||||||
//交换数字
|
```csharp
|
||||||
void Swap(ref int a, ref int b)
|
void Swap(ref int a, ref int b)
|
||||||
{
|
{
|
||||||
int temp = a; a = b; b = temp;
|
int temp = a;
|
||||||
}
|
a = b;
|
||||||
|
b = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = 10;
|
||||||
|
int y = 20;
|
||||||
|
Swap(ref x, ref y);
|
||||||
|
Console.WriteLine($"{x}, {y}"); // 20, 10
|
||||||
```
|
```
|
||||||
out:
|
|
||||||
```Csharp
|
`out`:
|
||||||
//最经典:int.TryParse
|
```csharp
|
||||||
if (int.TryParse(Console.ReadLine(), out int value))
|
bool TryGetAge(string text, out int age)
|
||||||
Console.WriteLine(value % 2 == 0 ? "even" : "odd");
|
{
|
||||||
//int.TryParse伪代码
|
return int.TryParse(text, out age);
|
||||||
//bool ReadConfig(string key, out string value)
|
}
|
||||||
//{
|
|
||||||
// if (配置存在)
|
if (TryGetAge("18", out int value))
|
||||||
// {
|
{
|
||||||
// value = 读取到的值; return true;
|
Console.WriteLine(value);
|
||||||
// }
|
}
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// value = null; return false;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
```
|
```
|
||||||
案例:
|
|
||||||
原型在[[值类型和引用类型]]笔记中
|
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
==out不是用来让内外值一定不等==
|
1. ==`ref` 参数必须先初始化==
|
||||||
==ref传入的参数必须初始化==
|
2. ==`out` 参数在方法结束前必须赋值==
|
||||||
如果传入引用类型且内外不同,推荐new一个
|
3. `ref` / `out` 传的是变量本身,所以修改会影响外部
|
||||||
|
4. 对引用类型来说:
|
||||||
|
- 不加 `ref` 时,方法里可以改对象内容
|
||||||
|
- 加 `ref` 时,连“变量指向哪个对象”都可以改
|
||||||
|
5. 属性、常量、表达式一般不能直接当 `ref` / `out` 参数传入
|
||||||
|
|
||||||
#基础
|
#基础
|
||||||
|
|||||||
+54
-39
@@ -1,46 +1,61 @@
|
|||||||
|
# 值类型和引用类型
|
||||||
概念:
|
概念:
|
||||||
==引用类型:string、数组、类==
|
- C# 的类型大体分成两类:==值类型== 和 ==引用类型==
|
||||||
==值类型:剩下都是,包括数组、enum、结构体==
|
- 值类型变量通常==直接保存数据本身==
|
||||||
|
- 引用类型变量保存的是==对象的引用(地址)==
|
||||||
值类型默认都是0
|
|
||||||
bool类型是false
|
常见类型:
|
||||||
引用类型默认值是null
|
- 值类型:数值类型、`bool`、`char`、`enum`、`struct`
|
||||||
|
- 引用类型:`string`、数组、类、接口、委托、`object`、`dynamic`
|
||||||
|
|
||||||
|
默认值:
|
||||||
|
- 数值类型默认值一般是 `0`
|
||||||
|
- `bool` 默认值是 `false`
|
||||||
|
- `char` 默认值是 `'\0'`
|
||||||
|
- `enum` 默认值是 `(枚举类型)0`
|
||||||
|
- 引用类型默认值一般是 `null`
|
||||||
|
|
||||||
使用区别:
|
使用区别:
|
||||||
值类型:在相互赋值时,把内容进行拷贝 //他变我不变
|
1. 赋值时
|
||||||
引用类型:把相互赋值时,把==地址==进行拷贝 //他变我也变
|
- 值类型:复制的是数据本身
|
||||||
原因:
|
- 引用类型:复制的是引用
|
||||||
值类型和引用类型 存储的内存区域不同,存取方式也不一样
|
2. 传参时
|
||||||
值类型:栈空间 —— 小但是快 (0GC)
|
- 默认也遵循上面的规则
|
||||||
引用类型:堆空间 —— 大但是慢 (且有GC)
|
- 所以值类型通常是“传一份副本”
|
||||||
|
- 引用类型通常是“传一份引用”
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
值类型:
|
值类型:
|
||||||
```Csharp
|
```csharp
|
||||||
static void ChangeValue(int value)
|
static void ChangeValue(int value)
|
||||||
{
|
{
|
||||||
value =3:
|
value = 99;
|
||||||
}
|
}
|
||||||
static void Main(string[] args)
|
|
||||||
{
|
int a = 1;
|
||||||
int a = 1;
|
ChangeValue(a);
|
||||||
ChangeValue(1);
|
Console.WriteLine(a); // 1
|
||||||
cw(a);
|
|
||||||
}
|
|
||||||
//仍然输出1
|
|
||||||
```
|
```
|
||||||
引用类型:
|
|
||||||
```Csharp
|
引用类型:
|
||||||
static void ChangeValue(int[] value)
|
```csharp
|
||||||
{
|
static void ChangeValue(int[] value)
|
||||||
value =99:
|
{
|
||||||
}
|
value[0] = 99;
|
||||||
static void Main(string[] args)
|
}
|
||||||
{
|
|
||||||
int[] a = {1};
|
int[] a = { 1 };
|
||||||
ChangeValue(a);
|
ChangeValue(a);
|
||||||
cw(a[0]);
|
Console.WriteLine(a[0]); // 99
|
||||||
}
|
|
||||||
//输出99
|
|
||||||
```
|
```
|
||||||
#基础
|
|
||||||
|
注意:
|
||||||
|
1. ==数组是引用类型,不是值类型==
|
||||||
|
2. ==string 也是引用类型==,但它有“不可变”特性
|
||||||
|
3. “值类型在栈上、引用类型在堆上”只是初学时的简化理解,==不适合当成绝对规则==
|
||||||
|
4. 更稳的记法是:
|
||||||
|
- 值类型变量保存值
|
||||||
|
- 引用类型变量保存引用
|
||||||
|
5. 如果想让方法直接改外部变量本身,要继续看[[ref和out]]
|
||||||
|
|
||||||
|
#基础
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# 万物之父和装箱与拆箱
|
||||||
|
# object
|
||||||
|
概念:
|
||||||
|
`object` 是 `System.Object` 的别名
|
||||||
|
在 C# 的统一类型系统里,可以把它理解成==万物之父==
|
||||||
|
因为==所有类型最终都能当成 `object` 来看待==
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 作为所有类型的公共父类型来理解 C# 类型系统
|
||||||
|
2. 在不知道具体类型时,临时用 `object` 接收数据
|
||||||
|
3. 理解为什么值类型可以被当成“对象”使用
|
||||||
|
|
||||||
|
核心理解:
|
||||||
|
1. 引用类型赋值给 `object` 时,通常只是==把引用当成 `object` 看待==
|
||||||
|
2. 值类型赋值给 `object` 时,会发生==装箱==
|
||||||
|
3. 所以“能放进 `object`”不代表底层过程都一样
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
string str = "hello";
|
||||||
|
object o1 = str; // 引用类型赋值给 object,不是装箱
|
||||||
|
|
||||||
|
int num = 10;
|
||||||
|
object o2 = num; // 值类型赋值给 object,发生装箱
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 为什么说 object 是万物之父
|
||||||
|
可以先这样记:
|
||||||
|
|
||||||
|
- 类最终都继承自 `object`
|
||||||
|
- 数组、委托等引用类型也都可以当成 `object`
|
||||||
|
- 值类型虽然不是普通类,但最终也能通过装箱当成 `object`
|
||||||
|
|
||||||
|
所以从使用角度看:
|
||||||
|
- ==所有类型最终都能统一到 `object` 这一层==
|
||||||
|
|
||||||
|
常见成员:
|
||||||
|
1. `ToString()`
|
||||||
|
把对象转成字符串
|
||||||
|
2. `Equals(object obj)`
|
||||||
|
比较两个对象是否相等
|
||||||
|
3. `GetType()`
|
||||||
|
获取对象运行时类型
|
||||||
|
4. `GetHashCode()`
|
||||||
|
获取哈希码
|
||||||
|
5. `ReferenceEquals(a, b)`
|
||||||
|
判断两个引用是不是指向同一个对象
|
||||||
|
|
||||||
|
例子:
|
||||||
|
```csharp
|
||||||
|
int num = 123;
|
||||||
|
object obj = num;
|
||||||
|
|
||||||
|
Console.WriteLine(obj.ToString());
|
||||||
|
Console.WriteLine(obj.GetType());
|
||||||
|
Console.WriteLine(obj.Equals(123));
|
||||||
|
```
|
||||||
|
|
||||||
|
补充:
|
||||||
|
- `ToString()`、`Equals()`、`GetType()`、`GetHashCode()` 是最常见的基础成员
|
||||||
|
- `ReferenceEquals` 是 `object` 提供的静态方法
|
||||||
|
|
||||||
|
----
|
||||||
|
# 装箱(Boxing)
|
||||||
|
概念:
|
||||||
|
装箱就是==把值类型转换成 `object` 或它实现的接口类型==
|
||||||
|
运行时会把这个值==复制一份==,包装成一个对象放到托管堆上
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 把值类型当成对象处理
|
||||||
|
2. 把值类型传给接收 `object` 的 API
|
||||||
|
3. 把值类型放进某些非泛型容器里
|
||||||
|
|
||||||
|
语法:
|
||||||
|
```csharp
|
||||||
|
int num = 10;
|
||||||
|
object obj = num; // 隐式装箱
|
||||||
|
object obj2 = (object)num; // 显式写法,但一般没必要
|
||||||
|
```
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
int a = 123;
|
||||||
|
object obj = a; // 装箱
|
||||||
|
|
||||||
|
a = 456;
|
||||||
|
|
||||||
|
Console.WriteLine(a); // 456
|
||||||
|
Console.WriteLine(obj); // 123
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
装箱时放进 `object` 的,是当时那个值的一份副本
|
||||||
|
所以后面 `a` 变了,不会影响已经装箱进去的 `obj`
|
||||||
|
|
||||||
|
注意:
|
||||||
|
1. ==装箱是值类型 -> `object` 或接口类型==
|
||||||
|
2. 装箱会产生额外对象分配,通常比普通赋值更耗性能
|
||||||
|
3. 引用类型赋值给 `object` 不叫装箱
|
||||||
|
|
||||||
|
----
|
||||||
|
# 拆箱(Unboxing)
|
||||||
|
概念:
|
||||||
|
拆箱就是==把 `object` 里的值类型取出来,还原成原来的值类型==
|
||||||
|
拆箱是==显式转换==
|
||||||
|
|
||||||
|
语法:
|
||||||
|
```csharp
|
||||||
|
object obj = 123; // 装箱
|
||||||
|
int num = (int)obj; // 拆箱
|
||||||
|
```
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
object obj = 100;
|
||||||
|
int num = (int)obj;
|
||||||
|
Console.WriteLine(num);
|
||||||
|
```
|
||||||
|
|
||||||
|
核心理解:
|
||||||
|
拆箱做的事不只是“强转一下”
|
||||||
|
它会先检查 `obj` 里面装着的是不是目标值类型
|
||||||
|
检查通过后,再把值复制出来
|
||||||
|
|
||||||
|
常见错误:
|
||||||
|
```csharp
|
||||||
|
object obj = 10;
|
||||||
|
short s = (short)obj; // 报错
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
`obj` 里装箱进去的真实类型是 `int`
|
||||||
|
拆箱时要求==类型先对上==
|
||||||
|
所以应该先拆成 `int`,再按需要继续转换
|
||||||
|
|
||||||
|
正确写法:
|
||||||
|
```csharp
|
||||||
|
object obj = 10;
|
||||||
|
short s = (short)(int)obj;
|
||||||
|
```
|
||||||
|
|
||||||
|
再补一个坑:
|
||||||
|
```csharp
|
||||||
|
object obj = null;
|
||||||
|
int num = (int)obj; // 运行时报错
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
1. ==拆箱一定是显式的==
|
||||||
|
2. ==拆箱时目标类型必须和装箱时的值类型匹配==
|
||||||
|
3. 对 `null` 拆箱会报错
|
||||||
|
4. 类型不匹配时会抛出转换异常
|
||||||
|
|
||||||
|
----
|
||||||
|
# object 和装箱拆箱的关系
|
||||||
|
最核心的一句话:
|
||||||
|
|
||||||
|
==值类型之所以也能被当成“对象”来处理,就是因为有装箱和拆箱==
|
||||||
|
|
||||||
|
对比理解:
|
||||||
|
```csharp
|
||||||
|
int num = 10;
|
||||||
|
object o1 = num; // 装箱
|
||||||
|
|
||||||
|
string str = "hello";
|
||||||
|
object o2 = str; // 不是装箱,只是引用转换
|
||||||
|
```
|
||||||
|
|
||||||
|
所以要分清:
|
||||||
|
1. 引用类型 -> `object`
|
||||||
|
通常只是“把它当父类看”
|
||||||
|
2. 值类型 -> `object`
|
||||||
|
通常会真的发生装箱
|
||||||
|
|
||||||
|
----
|
||||||
|
# 性能和使用建议
|
||||||
|
为什么要注意装箱拆箱:
|
||||||
|
|
||||||
|
1. 装箱会分配新对象
|
||||||
|
2. 拆箱要做类型检查和复制
|
||||||
|
3. 大量发生时,会增加 GC 压力和性能开销
|
||||||
|
|
||||||
|
常见容易发生装箱的场景:
|
||||||
|
1. 使用非泛型集合,比如 `ArrayList`
|
||||||
|
2. 方法参数写成 `object`
|
||||||
|
3. 值类型转成接口类型时
|
||||||
|
|
||||||
|
示意:
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
list.Add(1); // 装箱
|
||||||
|
list.Add(2); // 装箱
|
||||||
|
|
||||||
|
int num = (int)list[0]; // 拆箱
|
||||||
|
```
|
||||||
|
|
||||||
|
更推荐:
|
||||||
|
```csharp
|
||||||
|
List<int> list = new List<int>();
|
||||||
|
list.Add(1);
|
||||||
|
list.Add(2);
|
||||||
|
```
|
||||||
|
|
||||||
|
这样通常可以避免很多不必要的装箱拆箱。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
1. ==`object` 是万物之父,所有类型最终都能统一到它这一层==
|
||||||
|
2. ==值类型进 `object` 叫装箱,从 `object` 还原值类型叫拆箱==
|
||||||
|
3. ==引用类型给 `object` 不等于装箱==
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [Built-in reference types - The object type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/reference-types)
|
||||||
|
2. Microsoft Learn: [Boxing and Unboxing (C# Programming Guide)](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing)
|
||||||
|
3. Microsoft Learn: [Types - C# language specification](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types)
|
||||||
|
4. Microsoft Learn: [Object.GetType Method](https://learn.microsoft.com/en-us/dotnet/api/system.object.gettype)
|
||||||
|
5. Microsoft Learn: [System.Object.ToString method](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-object-tostring)
|
||||||
|
6. Microsoft Learn: [System.Object.Equals method](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-object-equals)
|
||||||
|
|
||||||
|
#核心
|
||||||
|
#object
|
||||||
|
#装箱拆箱
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
# 命名空间
|
||||||
|
概念:
|
||||||
|
命名空间使用 `namespace` 关键字定义
|
||||||
|
作用是:==组织代码,避免类型名字冲突==
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
命名空间就像文件夹
|
||||||
|
同名文件放在不同文件夹里不会冲突
|
||||||
|
同名类放在不同命名空间里也不会冲突
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 按功能或模块组织代码
|
||||||
|
2. 避免不同类库之间出现同名类型冲突
|
||||||
|
3. 让大型项目结构更清楚
|
||||||
|
4. 配合 `using` 简化类型书写
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
传统写法:
|
||||||
|
```csharp
|
||||||
|
namespace 命名空间名
|
||||||
|
{
|
||||||
|
class 类名
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
namespace Game.Character
|
||||||
|
{
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
文件范围命名空间写法:
|
||||||
|
```csharp
|
||||||
|
namespace Game.Character;
|
||||||
|
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这种写法表示:
|
||||||
|
当前文件中的类型都属于 `Game.Character` 命名空间
|
||||||
|
|
||||||
|
初学阶段先掌握传统大括号写法即可。
|
||||||
|
|
||||||
|
----
|
||||||
|
# using 的作用
|
||||||
|
如果类型在某个命名空间中,使用前可以写完整名称。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
System.Console.WriteLine("Hello");
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以先写 `using`:
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
|
||||||
|
Console.WriteLine("Hello");
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
`using System;` 表示引入 `System` 命名空间
|
||||||
|
这样就不用每次都写 `System.Console`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 避免同名冲突
|
||||||
|
```csharp
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Tool
|
||||||
|
{
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这两个 `Player` 不冲突,因为它们在不同命名空间中。
|
||||||
|
|
||||||
|
使用完整名称:
|
||||||
|
```csharp
|
||||||
|
Game.Player p1 = new Game.Player();
|
||||||
|
Tool.Player p2 = new Tool.Player();
|
||||||
|
```
|
||||||
|
|
||||||
|
如果只写:
|
||||||
|
```csharp
|
||||||
|
Player p = new Player();
|
||||||
|
```
|
||||||
|
|
||||||
|
编译器可能不知道你要用哪个 `Player`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 命名空间可以分开写
|
||||||
|
同一个命名空间可以分布在多个文件中。
|
||||||
|
|
||||||
|
文件1:
|
||||||
|
```csharp
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
文件2:
|
||||||
|
```csharp
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
class Enemy
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
这两个类型都属于 `Game` 命名空间
|
||||||
|
命名空间不是文件夹本身,而是代码里的逻辑组织方式
|
||||||
|
|
||||||
|
----
|
||||||
|
# 命名空间可以嵌套
|
||||||
|
```csharp
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
namespace Character
|
||||||
|
{
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
更常见的写法:
|
||||||
|
```csharp
|
||||||
|
namespace Game.Character
|
||||||
|
{
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
两种写法都表示:
|
||||||
|
`Player` 属于 `Game.Character`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 命名空间和访问修饰符
|
||||||
|
命名空间只是组织代码,不代表访问权限。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
public class Player
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`public`、`private`、`internal` 这些才是控制访问权限的关键。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
1. 顶级类一般不能直接写 `private`
|
||||||
|
2. 顶级类不写访问修饰符时,默认通常是 `internal`
|
||||||
|
3. 命名空间不负责决定谁能访问谁
|
||||||
|
|
||||||
|
----
|
||||||
|
# using 别名
|
||||||
|
如果两个命名空间中有同名类,可以用别名简化。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GamePlayer = Game.Player;
|
||||||
|
using ToolPlayer = Tool.Player;
|
||||||
|
|
||||||
|
GamePlayer p1 = new GamePlayer();
|
||||||
|
ToolPlayer p2 = new ToolPlayer();
|
||||||
|
```
|
||||||
|
|
||||||
|
这个写法不常用,但遇到同名类型冲突时很有用。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见错误
|
||||||
|
错误1:以为文件夹名就是命名空间
|
||||||
|
|
||||||
|
实际:
|
||||||
|
文件夹名和命名空间名可以一致,但不是同一个东西
|
||||||
|
真正决定命名空间的是代码里的 `namespace`
|
||||||
|
|
||||||
|
----
|
||||||
|
错误2:忘记写 `using`
|
||||||
|
```csharp
|
||||||
|
Console.WriteLine("Hello");
|
||||||
|
```
|
||||||
|
|
||||||
|
如果没有引入 `System`,可能找不到 `Console`。
|
||||||
|
|
||||||
|
解决:
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
```
|
||||||
|
|
||||||
|
或者:
|
||||||
|
```csharp
|
||||||
|
System.Console.WriteLine("Hello");
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
错误3:同名类型不知道用哪个
|
||||||
|
```csharp
|
||||||
|
using Game;
|
||||||
|
using Tool;
|
||||||
|
|
||||||
|
Player p = new Player();
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `Game` 和 `Tool` 里都有 `Player`,这里可能会冲突。
|
||||||
|
|
||||||
|
解决:
|
||||||
|
```csharp
|
||||||
|
Game.Player p = new Game.Player();
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 什么时候需要命名空间
|
||||||
|
小练习:
|
||||||
|
可以不太关注命名空间,直接写类也能运行
|
||||||
|
|
||||||
|
项目变大后:
|
||||||
|
1. 不同模块类越来越多
|
||||||
|
2. 第三方库越来越多
|
||||||
|
3. 同名类型冲突概率增加
|
||||||
|
4. 需要清晰分层和组织
|
||||||
|
|
||||||
|
这时就应该认真使用命名空间。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. `namespace` 用来组织代码和避免命名冲突
|
||||||
|
2. `using` 用来引入命名空间,简化类型名称
|
||||||
|
3. 不同命名空间里可以有同名类型
|
||||||
|
4. 使用完整类型名可以解决同名冲突
|
||||||
|
5. 命名空间可以跨多个文件
|
||||||
|
6. 命名空间可以嵌套,常用点号表示层级
|
||||||
|
7. 命名空间不是访问权限控制工具
|
||||||
|
8. 文件夹结构和命名空间通常保持一致,但语法上不是一回事
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==namespace 负责给类型分组,using 负责把分组引进来方便使用。==
|
||||||
|
|
||||||
|
再压缩一点:
|
||||||
|
==命名空间像文件夹,用来防止重名。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“命名空间是用 `namespace` 定义的代码组织方式,主要作用是给类型分组并避免命名冲突。
|
||||||
|
使用类型时可以写完整命名空间,也可以通过 `using` 引入命名空间来简化书写。
|
||||||
|
命名空间可以跨多个文件,也可以嵌套,但它本身不控制访问权限。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [namespace (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/namespace)
|
||||||
|
2. Microsoft Learn: [using directive (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive)
|
||||||
|
|
||||||
|
#命名空间
|
||||||
|
#核心
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
# 多态 vob
|
||||||
|
概念:
|
||||||
|
多态是面向对象三大特性之一
|
||||||
|
指的是:==同一个父类引用,指向不同子类对象时,调用同一个方法会表现出不同结果==
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
调用方只知道“这是一个动物”,但真正执行时,会根据对象实际是狗、猫还是鸟,执行各自的叫声
|
||||||
|
|
||||||
|
`vob` 可以这样记:
|
||||||
|
- `v`:`virtual`,父类声明虚方法
|
||||||
|
- `o`:`override`,子类重写父类虚方法
|
||||||
|
- `b`:`base`,子类中调用父类原本的成员
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 父类提供统一入口
|
||||||
|
2. 子类根据自己的特点重写行为
|
||||||
|
3. 调用方只面向父类或接口写代码,不关心具体子类
|
||||||
|
4. 减少大量 `if/else` 类型判断
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
父类:
|
||||||
|
```csharp
|
||||||
|
class 父类名
|
||||||
|
{
|
||||||
|
public virtual void 方法名()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
子类:
|
||||||
|
```csharp
|
||||||
|
class 子类名 : 父类名
|
||||||
|
{
|
||||||
|
public override void 方法名()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
调用:
|
||||||
|
```csharp
|
||||||
|
父类名 变量名 = new 子类名();
|
||||||
|
变量名.方法名();
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public virtual void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("动物发出声音");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗:汪汪");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cat : Animal
|
||||||
|
{
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("猫:喵喵");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animal a1 = new Dog();
|
||||||
|
Animal a2 = new Cat();
|
||||||
|
|
||||||
|
a1.Speak(); // 狗:汪汪
|
||||||
|
a2.Speak(); // 猫:喵喵
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
- `a1` 和 `a2` 的变量类型都是 `Animal`
|
||||||
|
- 但是它们实际指向的对象分别是 `Dog` 和 `Cat`
|
||||||
|
- 调用 `Speak()` 时,会执行实际对象对应的重写版本
|
||||||
|
|
||||||
|
这就是多态。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 为什么要用多态
|
||||||
|
不用多态时,代码可能会这样:
|
||||||
|
```csharp
|
||||||
|
void MakeSpeak(Animal animal)
|
||||||
|
{
|
||||||
|
if (animal is Dog)
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗:汪汪");
|
||||||
|
}
|
||||||
|
else if (animal is Cat)
|
||||||
|
{
|
||||||
|
Console.WriteLine("猫:喵喵");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
问题:
|
||||||
|
1. 每增加一个新动物,就要改 `MakeSpeak`
|
||||||
|
2. 判断会越来越多
|
||||||
|
3. 行为分散在外部函数里,子类自己的特点没有放回子类中
|
||||||
|
|
||||||
|
使用多态后:
|
||||||
|
```csharp
|
||||||
|
void MakeSpeak(Animal animal)
|
||||||
|
{
|
||||||
|
animal.Speak();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
以后新增子类:
|
||||||
|
```csharp
|
||||||
|
class Bird : Animal
|
||||||
|
{
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("鸟:叽叽");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
调用方不用改:
|
||||||
|
```csharp
|
||||||
|
MakeSpeak(new Bird());
|
||||||
|
```
|
||||||
|
|
||||||
|
这就是多态最重要的价值:
|
||||||
|
==把变化交给子类,把稳定入口留给父类==
|
||||||
|
|
||||||
|
----
|
||||||
|
# virtual
|
||||||
|
作用:
|
||||||
|
`virtual` 用在父类成员上,表示这个成员允许被子类重写
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Character
|
||||||
|
{
|
||||||
|
public virtual void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("角色攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
1. 没有写 `virtual` 的普通方法,子类不能用 `override` 重写
|
||||||
|
2. `virtual` 常用于方法、属性、索引器、事件
|
||||||
|
3. 构造函数不能是 `virtual`
|
||||||
|
4. 静态方法不能被 `override`
|
||||||
|
|
||||||
|
----
|
||||||
|
# override
|
||||||
|
作用:
|
||||||
|
`override` 用在子类成员上,表示重写父类的虚成员
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Warrior : Character
|
||||||
|
{
|
||||||
|
public override void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("战士挥剑攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
重写要求:
|
||||||
|
1. 父类成员必须是 `virtual`、`abstract` 或已经是 `override`
|
||||||
|
2. 子类方法名、参数列表、返回类型要匹配
|
||||||
|
3. 访问修饰符通常要保持一致,例如父类是 `public`,子类也要是 `public`
|
||||||
|
4. `override` 之后,这个方法仍然可以继续被更下一级子类重写,除非加 `sealed`
|
||||||
|
|
||||||
|
----
|
||||||
|
# base
|
||||||
|
作用:
|
||||||
|
`base` 用在子类内部,表示访问父类成员
|
||||||
|
|
||||||
|
常见用途:
|
||||||
|
1. 调用父类构造函数
|
||||||
|
2. 在重写方法中,先执行父类原本逻辑,再补充子类逻辑
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Character
|
||||||
|
{
|
||||||
|
public virtual void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("消耗体力");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Warrior : Character
|
||||||
|
{
|
||||||
|
public override void Attack()
|
||||||
|
{
|
||||||
|
base.Attack();
|
||||||
|
Console.WriteLine("战士挥剑攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Warrior w = new Warrior();
|
||||||
|
w.Attack();
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:
|
||||||
|
```text
|
||||||
|
消耗体力
|
||||||
|
战士挥剑攻击
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
`base.Attack()` 不是创建父类对象,而是在子类方法里调用父类原本的 `Attack`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 多态调用看实际对象
|
||||||
|
多态最关键的一句:
|
||||||
|
==编译时看变量类型,运行时看实际对象==
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Animal animal = new Dog();
|
||||||
|
animal.Speak();
|
||||||
|
```
|
||||||
|
|
||||||
|
这里:
|
||||||
|
1. 编译时,`animal` 的类型是 `Animal`
|
||||||
|
2. 所以只能直接调用 `Animal` 中声明过的成员
|
||||||
|
3. 运行时,`animal` 实际指向的是 `Dog`
|
||||||
|
4. 所以 `Speak()` 执行的是 `Dog` 重写后的版本
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```csharp
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗叫");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Bite()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗咬人");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animal animal = new Dog();
|
||||||
|
animal.Speak(); // 可以,多态调用
|
||||||
|
// animal.Bite(); // 错误,Animal 类型里没有 Bite
|
||||||
|
```
|
||||||
|
|
||||||
|
如果确实要调用 `Dog` 独有方法,需要类型转换:
|
||||||
|
```csharp
|
||||||
|
Dog dog = animal as Dog;
|
||||||
|
if (dog != null)
|
||||||
|
{
|
||||||
|
dog.Bite();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
但如果代码经常需要这样转,通常要回头检查抽象设计是不是不够好。
|
||||||
|
|
||||||
|
----
|
||||||
|
# override 和 new 的区别
|
||||||
|
`override` 是真正的多态重写。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Father
|
||||||
|
{
|
||||||
|
public virtual void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Father");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Son : Father
|
||||||
|
{
|
||||||
|
public override void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Son");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Father f = new Son();
|
||||||
|
f.Say(); // Son
|
||||||
|
```
|
||||||
|
|
||||||
|
`new` 是隐藏父类成员,不是真正的多态。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Father
|
||||||
|
{
|
||||||
|
public void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Father");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Son : Father
|
||||||
|
{
|
||||||
|
public new void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Son");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Father f = new Son();
|
||||||
|
f.Say(); // Father
|
||||||
|
```
|
||||||
|
|
||||||
|
区别:
|
||||||
|
1. `override`:看实际对象,所以会调用 `Son.Say()`
|
||||||
|
2. `new`:看变量类型,所以 `Father f` 调用的是 `Father.Say()`
|
||||||
|
3. 想要多态效果,用 `virtual + override`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见错误
|
||||||
|
错误1:父类没写 `virtual`,子类却写 `override`
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public void Speak()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
错误原因:
|
||||||
|
父类的 `Speak` 不是虚方法,不能被重写
|
||||||
|
|
||||||
|
----
|
||||||
|
错误2:以为子类独有成员也能通过父类变量直接调用
|
||||||
|
```csharp
|
||||||
|
Animal animal = new Dog();
|
||||||
|
// animal.Bite(); // 错误
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
父类变量只能看到父类声明过的成员
|
||||||
|
|
||||||
|
----
|
||||||
|
错误3:用 `new` 代替 `override`
|
||||||
|
```csharp
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public new void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样只是隐藏,不是真正的多态。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和里氏替换原则的关系
|
||||||
|
多态能成立的前提是:
|
||||||
|
==子类真的能当父类来用==
|
||||||
|
|
||||||
|
比如:
|
||||||
|
```csharp
|
||||||
|
void MakeSpeak(Animal animal)
|
||||||
|
{
|
||||||
|
animal.Speak();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个函数相信:所有 `Animal` 都能 `Speak()`。
|
||||||
|
|
||||||
|
如果某个子类重写后直接抛异常,或者完全破坏父类原本承诺的行为,就会影响[[里氏替换原则]]。
|
||||||
|
|
||||||
|
所以可以这样联系:
|
||||||
|
1. [[继承]]提供父子类型关系
|
||||||
|
2. 多态让同一个调用产生不同表现
|
||||||
|
3. [[里氏替换原则]]约束子类不要乱改父类承诺
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. 多态主要依赖 `virtual + override`
|
||||||
|
2. 父类引用可以指向子类对象,这是多态的基础
|
||||||
|
3. 调用重写方法时,看的是对象的实际类型
|
||||||
|
4. 父类变量只能访问父类中声明过的成员
|
||||||
|
5. `base` 是在子类中访问父类成员,不是创建父类对象
|
||||||
|
6. `new` 是隐藏成员,不等于 `override`
|
||||||
|
7. 如果父类方法不希望再被重写,可以在子类重写时写 `sealed override`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==父类定规则,子类改表现,调用时看实际对象。==
|
||||||
|
|
||||||
|
再压缩一点:
|
||||||
|
==virtual 允许改,override 真正改,base 调父类。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“多态是指同一个父类引用指向不同子类对象时,调用同一个虚方法会执行不同子类的重写实现。
|
||||||
|
在 C# 中通常通过 `virtual` 和 `override` 实现,父类用 `virtual` 声明可重写方法,子类用 `override` 提供自己的实现。
|
||||||
|
如果子类重写时还想保留父类原本逻辑,可以用 `base` 调用父类方法。多态的好处是调用方可以面向父类编程,新增子类时尽量少改原有代码。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [Polymorphism](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/polymorphism)
|
||||||
|
2. Microsoft Learn: [virtual (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/virtual)
|
||||||
|
3. Microsoft Learn: [override (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/override)
|
||||||
|
4. Microsoft Learn: [The base keyword](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/base)
|
||||||
|
5. Microsoft Learn: [Knowing When to Use Override and New Keywords](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords)
|
||||||
|
|
||||||
|
#多态
|
||||||
|
#核心
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
# 密封函数
|
||||||
|
概念:
|
||||||
|
密封函数指的是使用 `sealed` 修饰的==重写成员==
|
||||||
|
在 C# 里常见写法是 `sealed override`
|
||||||
|
|
||||||
|
准确一点说:
|
||||||
|
C# 里通常叫“密封方法”或“密封重写成员”
|
||||||
|
有些教程会说“密封函数”,初学阶段可以理解成:==不允许下一级子类继续重写的方法==
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 父类允许子类重写某个方法
|
||||||
|
2. 某个子类重写后,认为这个实现已经定死
|
||||||
|
3. 允许这个子类继续被继承
|
||||||
|
4. 但不允许更下一级子类再改这个方法
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
```csharp
|
||||||
|
class 子类名 : 父类名
|
||||||
|
{
|
||||||
|
public sealed override void 方法名()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
`sealed` 修饰方法时,必须和 `override` 一起使用
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public virtual void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("动物叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public sealed override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Husky : Dog
|
||||||
|
{
|
||||||
|
// 这里不能再 override Speak()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `Animal.Speak()` 是虚方法,允许被重写
|
||||||
|
2. `Dog.Speak()` 重写了父类方法
|
||||||
|
3. `Dog.Speak()` 又被 `sealed` 密封
|
||||||
|
4. `Husky` 可以继承 `Dog`
|
||||||
|
5. 但 `Husky` 不能继续重写 `Speak()`
|
||||||
|
|
||||||
|
----
|
||||||
|
# sealed class 和 sealed override 的区别
|
||||||
|
密封类:
|
||||||
|
```csharp
|
||||||
|
sealed class Dog : Animal
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
含义:
|
||||||
|
整个 `Dog` 类不能再被继承
|
||||||
|
|
||||||
|
密封函数:
|
||||||
|
```csharp
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public sealed override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
含义:
|
||||||
|
`Dog` 类仍然可以被继承
|
||||||
|
但是 `Speak()` 不能再被下一级子类重写
|
||||||
|
|
||||||
|
对比:
|
||||||
|
1. `sealed class`:封住整个类
|
||||||
|
2. `sealed override`:只封住某个已经重写的成员
|
||||||
|
|
||||||
|
这个点可以和[[密封类]]一起记。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 为什么密封函数必须是 override
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
class Dog
|
||||||
|
{
|
||||||
|
public sealed void Speak()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
普通方法本来就不能被 `override`
|
||||||
|
没有必要再用 `sealed` 封住
|
||||||
|
|
||||||
|
正确理解:
|
||||||
|
`sealed` 修饰方法时,是在说:
|
||||||
|
“这个方法原本处在可重写链条上,但从我这里开始不准继续重写”
|
||||||
|
|
||||||
|
所以它必须修饰已经重写的成员,也就是 `sealed override`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 多级继承案例
|
||||||
|
```csharp
|
||||||
|
class A
|
||||||
|
{
|
||||||
|
public virtual void Test()
|
||||||
|
{
|
||||||
|
Console.WriteLine("A");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class B : A
|
||||||
|
{
|
||||||
|
public override void Test()
|
||||||
|
{
|
||||||
|
Console.WriteLine("B");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C : B
|
||||||
|
{
|
||||||
|
public sealed override void Test()
|
||||||
|
{
|
||||||
|
Console.WriteLine("C");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class D : C
|
||||||
|
{
|
||||||
|
// public override void Test()
|
||||||
|
// {
|
||||||
|
// Console.WriteLine("D");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
调用:
|
||||||
|
```csharp
|
||||||
|
A a = new C();
|
||||||
|
a.Test(); // C
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `A.Test()` 开启虚方法
|
||||||
|
2. `B.Test()` 继续重写
|
||||||
|
3. `C.Test()` 重写并密封
|
||||||
|
4. `D` 仍然可以继承 `C`
|
||||||
|
5. 但 `D` 不能再重写 `Test()`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 密封属性
|
||||||
|
`sealed override` 不只可以用于方法,也可以用于属性。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Character
|
||||||
|
{
|
||||||
|
public virtual int MaxHp
|
||||||
|
{
|
||||||
|
get { return 100; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Boss : Character
|
||||||
|
{
|
||||||
|
public sealed override int MaxHp
|
||||||
|
{
|
||||||
|
get { return 1000; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
后续子类不能再重写 `MaxHp`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和多态的关系
|
||||||
|
密封函数不会取消已有多态效果。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Animal animal = new Dog();
|
||||||
|
animal.Speak(); // 狗叫
|
||||||
|
```
|
||||||
|
|
||||||
|
`Dog.Speak()` 仍然会通过多态被调用。
|
||||||
|
|
||||||
|
`sealed override` 只是限制:
|
||||||
|
后续子类不能继续改这个方法
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
1. 对上层父类来说,它仍然是重写后的多态方法
|
||||||
|
2. 对下层子类来说,它已经不能再被重写
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见错误
|
||||||
|
错误1:普通方法前写 `sealed`
|
||||||
|
```csharp
|
||||||
|
class A
|
||||||
|
{
|
||||||
|
public sealed void Test()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`sealed` 不能这样修饰普通方法。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误2:只写 `sealed` 不写 `override`
|
||||||
|
```csharp
|
||||||
|
class B : A
|
||||||
|
{
|
||||||
|
public sealed void Test()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
密封重写成员必须写成 `sealed override`。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误3:父类方法不是虚方法
|
||||||
|
```csharp
|
||||||
|
class A
|
||||||
|
{
|
||||||
|
public void Test()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class B : A
|
||||||
|
{
|
||||||
|
public sealed override void Test()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
父类方法不是 `virtual` / `abstract` / `override`,子类不能 `override`,也就不能 `sealed override`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 什么时候用密封函数
|
||||||
|
适合用的情况:
|
||||||
|
1. 某个重写逻辑非常关键,不希望后续子类改掉
|
||||||
|
2. 父类设计允许多态,但某一层子类希望终止这个方法的重写链
|
||||||
|
3. 框架或基础类库中需要保证关键行为稳定
|
||||||
|
|
||||||
|
初学阶段:
|
||||||
|
先知道这个语法和作用即可
|
||||||
|
自己写练习代码时用得不多
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. 密封函数常见写法是 `sealed override`
|
||||||
|
2. 它只能用于已经重写的成员
|
||||||
|
3. 它阻止下一级子类继续重写这个成员
|
||||||
|
4. 它不会阻止当前类被继承
|
||||||
|
5. 如果想阻止整个类被继承,用 `sealed class`
|
||||||
|
6. 方法、属性、索引器、事件等重写成员都可以理解这个规则
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==sealed override 表示这个重写到我这里为止,后面的子类不能再改。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“密封函数通常指 `sealed override` 修饰的重写方法。
|
||||||
|
它表示当前类重写了父类的虚方法,但不允许更下一级子类继续重写。
|
||||||
|
它和密封类不同,密封类是不允许整个类被继承,而密封函数只是禁止某个重写成员继续被重写。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [sealed (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed)
|
||||||
|
2. Microsoft Learn: [override (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/override)
|
||||||
|
|
||||||
|
#密封函数
|
||||||
|
#密封方法
|
||||||
|
#核心
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# 密封类
|
||||||
|
概念:
|
||||||
|
使用 `sealed` 关键字修饰的类,叫做==密封类==
|
||||||
|
作用是:==让这个类不能再被其他类继承==
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
这个类已经是“最终版本”了,别人只能使用它,不能在它的基础上继续派生子类
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 不希望某个类被继续继承,避免子类乱改行为
|
||||||
|
2. 类的设计已经完整,不打算给别人扩展
|
||||||
|
3. 保护程序规范性和安全性
|
||||||
|
4. 表示某个类型在业务上就是终点,不应该再有更细的子类
|
||||||
|
|
||||||
|
语法:
|
||||||
|
```csharp
|
||||||
|
sealed class 类名
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
class Enemy
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public virtual void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("敌人攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Boss : Enemy
|
||||||
|
{
|
||||||
|
public override void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Boss发动攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
上面代码中:
|
||||||
|
- `Boss` 可以继承 `Enemy`
|
||||||
|
- 但是 `Boss` 被 `sealed` 修饰后,别的类就不能再继承 `Boss`
|
||||||
|
|
||||||
|
错误示意:
|
||||||
|
```csharp
|
||||||
|
class FinalBoss : Boss
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样会报错,因为 `Boss` 是密封类,不能作为父类继续被继承
|
||||||
|
|
||||||
|
----
|
||||||
|
# 密封类还能不能正常使用
|
||||||
|
可以。
|
||||||
|
|
||||||
|
`sealed` 只是不允许别人继承它,==不影响创建对象和调用成员==。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
sealed class PlayerConfig
|
||||||
|
{
|
||||||
|
public int MaxHp = 100;
|
||||||
|
|
||||||
|
public void Print()
|
||||||
|
{
|
||||||
|
Console.WriteLine(MaxHp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerConfig config = new PlayerConfig();
|
||||||
|
config.Print();
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 密封类和继承
|
||||||
|
密封类不是说“自己不能继承别人”,而是说“别人不能继承自己”。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Dog : Animal
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个是可以的:
|
||||||
|
- `Dog` 继承了 `Animal`
|
||||||
|
- `Dog` 自己被密封
|
||||||
|
- 所以以后不能再写 `class Husky : Dog`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和普通类的区别
|
||||||
|
普通类:
|
||||||
|
```csharp
|
||||||
|
class A
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class B : A
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
密封类:
|
||||||
|
```csharp
|
||||||
|
sealed class A
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// class B : A // 错误,A 不能被继承
|
||||||
|
```
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
普通类可以当父类,密封类不能当父类
|
||||||
|
|
||||||
|
----
|
||||||
|
# 密封类和抽象类不能同时用
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
sealed abstract class A
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
- `abstract` 抽象类的意义是:必须被子类继承后补全实现
|
||||||
|
- `sealed` 密封类的意义是:不允许被继承
|
||||||
|
|
||||||
|
这两个要求互相矛盾,所以不能一起用
|
||||||
|
|
||||||
|
----
|
||||||
|
# 顺带理解:sealed 也能修饰重写成员
|
||||||
|
`sealed` 不只可以修饰类,还可以修饰==已经 override 的方法或属性==。
|
||||||
|
|
||||||
|
作用是:
|
||||||
|
允许这个类继续被继承,但是不允许子类继续重写某个成员
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public virtual void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("动物叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public sealed override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Husky : Dog
|
||||||
|
{
|
||||||
|
// 这里不能再 override Speak
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里要区分:
|
||||||
|
1. `sealed class Dog`:整个 `Dog` 类不能被继承
|
||||||
|
2. `sealed override void Speak()`:`Dog` 类还能被继承,但 `Speak` 不能继续被重写
|
||||||
|
|
||||||
|
这个内容后面可以和[[多态vob|多态]]、[[密封函数]]一起理解。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. `sealed` 修饰类时,表示这个类==不能被继承==
|
||||||
|
2. 密封类自己仍然可以继承普通类,也可以实现接口
|
||||||
|
3. 密封类可以正常创建对象、调用方法、访问属性
|
||||||
|
4. 密封类不能和 `abstract` 同时使用
|
||||||
|
5. 如果只是想禁止某个方法继续被重写,可以用 `sealed override`,不一定要把整个类密封
|
||||||
|
6. 初学阶段不要滥用 `sealed`,只有确定“不应该再被继承”时再用
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==sealed class 表示这个类到此为止,后面不能再派生子类。==
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [sealed (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed)
|
||||||
|
2. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
|
||||||
|
|
||||||
|
#密封类
|
||||||
|
#核心
|
||||||
+55
-41
@@ -1,55 +1,69 @@
|
|||||||
|
# 成员变量和访问修饰符
|
||||||
概念:
|
概念:
|
||||||
1. 声明在类中
|
1. 成员变量也常叫==字段(field)==
|
||||||
2. 用来描述对象特征
|
2. 它声明在类或结构体中,用来描述对象状态
|
||||||
3. 任意变量类型
|
3. 字段类型可以是任意合法类型
|
||||||
4. 是否赋值根据需求决定
|
4. 字段如果没手动赋值,会先使用该类型的默认值
|
||||||
|
|
||||||
|
|
||||||
通常用法:
|
通常用法:
|
||||||
|
- 保存对象的基础数据
|
||||||
|
- 配合访问修饰符控制谁能访问
|
||||||
|
- 再配合[[成员属性]]对外暴露更安全的访问方式
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
1. 声明:
|
```csharp
|
||||||
```Csharp
|
class Human
|
||||||
enum E_SexType
|
{
|
||||||
{
|
public string name;
|
||||||
male,
|
private int age;
|
||||||
female
|
protected bool isAlive;
|
||||||
}
|
internal Human[] friends;
|
||||||
class Human
|
}
|
||||||
{
|
|
||||||
string name;
|
|
||||||
int age;
|
|
||||||
E_Sex sex;
|
|
||||||
Human[] friend; //==可以类里声明自己类的同类型 但是不能实例化!==
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
2. 访问修饰符
|
|
||||||
public——公共的 类内部和外部都可以进行访问
|
访问修饰符:
|
||||||
private——私有的 只有内部才能访问 ==子类不行==
|
1. `public`
|
||||||
protected——保护的 只有自己内部和子类才可以访问
|
类内部和外部都能访问
|
||||||
3. 成员默认值
|
2. `private`
|
||||||
可以看[[值类型和引用类型]]
|
只有当前类型内部能访问
|
||||||
懒得看可以cw(default(变量类型))
|
3. `protected`
|
||||||
|
当前类型和子类能访问
|
||||||
|
4. `internal`
|
||||||
|
同一个程序集内能访问
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
```Csharp
|
```csharp
|
||||||
class Human
|
class Human
|
||||||
{
|
{
|
||||||
string name;
|
public string name;
|
||||||
int age;
|
private int age;
|
||||||
E_Sex sex;
|
|
||||||
Human[] friend; //==可以类里声明自己类的同类型 但是不能实例化!==
|
public void SetAge(int value)
|
||||||
}
|
{
|
||||||
Human h1 = new Human();
|
age = value;
|
||||||
h1.age = 18;
|
}
|
||||||
cw(h1.age);
|
|
||||||
|
public void PrintInfo()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{name} - {age}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Human h1 = new Human();
|
||||||
|
h1.name = "小明";
|
||||||
|
// h1.age = 18; // 报错,外部不能直接访问 private 字段
|
||||||
|
h1.SetAge(18);
|
||||||
|
h1.PrintInfo();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
|
1. ==类中的字段如果不写访问修饰符,默认是 `private`==
|
||||||
|
2. 字段可以声明成当前类型本身,比如 `Human[] friends`
|
||||||
|
3. 默认值示例:
|
||||||
|
- `int` 是 `0`
|
||||||
|
- `bool` 是 `false`
|
||||||
|
- 引用类型是 `null`
|
||||||
|
4. 实际开发中,很多字段会写成 `private`,再通过[[成员属性]]控制访问
|
||||||
|
|
||||||
#封装
|
#封装
|
||||||
#核心
|
#核心
|
||||||
|
|
||||||
|
|||||||
+61
-63
@@ -1,90 +1,88 @@
|
|||||||
|
# 成员属性
|
||||||
概念:
|
概念:
|
||||||
==和成员变量不完全是一个概念!!!!==
|
- 属性(Property)不是字段本身,而是==对字段的访问入口==
|
||||||
1. 用于保护成员变量
|
- 它看起来像变量,用起来也像变量,但本质上是特殊方法
|
||||||
2. 为成员属性的获取和复制添加逻辑处理
|
- 属性常用于:读取、赋值、校验、限制访问
|
||||||
3. 解决3P局限性
|
|
||||||
自动属性
|
|
||||||
|
|
||||||
通常用法:
|
通常用法:
|
||||||
配合private加密或规范成员属性的修改获取等
|
1. 保护字段,不让外部随便乱改
|
||||||
让其==只能获取不能修改== 或 ==只能修改不能获取==
|
2. 在赋值或读取时加入逻辑
|
||||||
|
3. 做“只能读”“只能类内改”“初始化后不可改”等控制
|
||||||
|
|
||||||
语法:
|
完整语法:
|
||||||
属性声明:
|
```csharp
|
||||||
```Csharp
|
class Person
|
||||||
访问修饰符 属性类型 属性名
|
|
||||||
{
|
|
||||||
get{}
|
|
||||||
set{}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
案例:
|
|
||||||
```Csharp
|
|
||||||
class Person()
|
|
||||||
{
|
{
|
||||||
private int age;
|
|
||||||
private string name;
|
private string name;
|
||||||
private int money;
|
|
||||||
private bool sex;
|
public string Name
|
||||||
public string Name [[命名法|帕斯卡命名法]]
|
|
||||||
{
|
{
|
||||||
get==必须有返回值==
|
get
|
||||||
{
|
{
|
||||||
//这里写处理获取内容的逻辑
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
//这里写处理修改/添加内容的逻辑,不写就没有
|
|
||||||
==//关键字value 用于表示外部传入的值 只在set中有==
|
|
||||||
name = value;
|
name = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
private int age;
|
||||||
|
|
||||||
|
public int Age
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
age = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
age = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Person p = new Person();
|
Person p = new Person();
|
||||||
~~p.name = xxx~~ 因为private所以没法用.name
|
p.Age = -5;
|
||||||
p.Name = xxx;
|
Console.WriteLine(p.Age); // 0
|
||||||
```
|
```
|
||||||
|
|
||||||
↑写了一个public的属性大家都可以访问,用该属性Name进行修改 return直接返回原始的name,set时使用.Name将xxx作为传入参数放到set的value关键字中
|
说明:
|
||||||
|
- `get` 负责取值
|
||||||
|
- `set` 负责赋值
|
||||||
|
- `set` 里的 `value` 表示外部传入的新值
|
||||||
|
|
||||||
案例2:
|
自动属性:
|
||||||
```Csharp
|
```csharp
|
||||||
public int Money()
|
public string Name { get; set; }
|
||||||
{
|
public float Height { get; private set; }
|
||||||
get
|
public string Id { get; init; }
|
||||||
{
|
|
||||||
return money;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(value<0) value = 0;
|
|
||||||
cw(没钱);
|
|
||||||
money = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
↑最简单的防止money为负数的逻辑
|
|
||||||
|
补充:
|
||||||
|
- 自动属性会由编译器生成隐藏的 backing field
|
||||||
|
- `private set` 表示外部能读、类内部能改
|
||||||
|
- `init` 表示只能在创建对象时赋值
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
==get必须有返回值==
|
1. ==`get` 中如果要返回值,就必须 `return`==
|
||||||
成员属性不加访问修饰符,会使用成员变量声明时的访问权限
|
2. 属性本身的访问级别看属性前面的修饰符
|
||||||
==get和set也可以加访问修饰符,但是不能大于成员属性前访问修饰符的权限==
|
3. `get` / `set` 可以单独再加更严格的访问修饰符,但不能比属性本身更开放
|
||||||
可以只有一个set或只有一个get(只能得或只能改),但是通常没人这么干
|
4. 只有 `get` 没有 `set` 时是只读属性
|
||||||
|
5. 只有 `set` 没有 `get` 时是只写属性,但实际开发里很少这样写
|
||||||
自动属性
|
6. 属性不是普通变量,所以通常不能当 `ref` / `out` 参数传入
|
||||||
相当于声明了一个成员变量,但是好处是可以添加访问修饰符
|
|
||||||
案例:可以得,但是不能改得一个成员↓
|
|
||||||
```Csharp
|
|
||||||
public float Height
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
#封装
|
#封装
|
||||||
#核心
|
#核心
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
概念:
|
||||||
|
使用 `abstract` 关键字修饰的类,叫做==抽象类==
|
||||||
|
使用 `abstract` 关键字修饰的方法,叫做==抽象方法==
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
抽象类像一份“不完整的父类模板”
|
||||||
|
它可以规定子类必须有什么能力,但具体怎么做,交给子类自己实现
|
||||||
|
|
||||||
|
补充:
|
||||||
|
有些教程会把“抽象方法”叫作“抽象函数”,初学阶段可以先理解成同一个意思
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 多个类有共同身份,但父类本身不应该被直接创建对象
|
||||||
|
2. 父类能提供一部分公共代码
|
||||||
|
3. 父类还想强制子类必须实现某些方法
|
||||||
|
4. 配合[[继承]]和[[多态vob|多态]],让调用方通过父类统一调用子类行为
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
抽象类:
|
||||||
|
```csharp
|
||||||
|
abstract class 类名
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象方法:
|
||||||
|
```csharp
|
||||||
|
abstract class 类名
|
||||||
|
{
|
||||||
|
public abstract void 方法名();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
子类实现抽象方法:
|
||||||
|
```csharp
|
||||||
|
class 子类名 : 抽象类名
|
||||||
|
{
|
||||||
|
public override void 方法名()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
抽象方法没有方法体,结尾直接写分号 `;`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
abstract class Animal
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Animal(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Eat()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "正在吃东西");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Speak();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public Dog(string name) : base(name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + ":汪汪");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cat : Animal
|
||||||
|
{
|
||||||
|
public Cat(string name) : base(name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + ":喵喵");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animal a1 = new Dog("旺财");
|
||||||
|
Animal a2 = new Cat("小花");
|
||||||
|
|
||||||
|
a1.Eat();
|
||||||
|
a1.Speak();
|
||||||
|
|
||||||
|
a2.Eat();
|
||||||
|
a2.Speak();
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `Animal` 是抽象类,不能直接 `new Animal()`
|
||||||
|
2. `Eat()` 是普通方法,子类可以直接继承使用
|
||||||
|
3. `Speak()` 是抽象方法,父类只规定“动物要会叫”
|
||||||
|
4. `Dog` 和 `Cat` 必须用 `override` 实现自己的 `Speak()`
|
||||||
|
5. `Animal a1 = new Dog()` 可以配合多态使用
|
||||||
|
|
||||||
|
----
|
||||||
|
# 抽象类不能直接实例化
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
abstract class Animal
|
||||||
|
{
|
||||||
|
public abstract void Speak();
|
||||||
|
}
|
||||||
|
|
||||||
|
Animal animal = new Animal();
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
抽象类是不完整的类型,里面可能有没实现的抽象成员
|
||||||
|
所以不能直接创建对象
|
||||||
|
|
||||||
|
正确用法:
|
||||||
|
```csharp
|
||||||
|
Animal animal = new Dog("旺财");
|
||||||
|
animal.Speak();
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 抽象方法必须在子类中实现
|
||||||
|
如果子类不是抽象类,就必须实现父类中所有抽象方法。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Shape
|
||||||
|
{
|
||||||
|
public abstract double GetArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Circle : Shape
|
||||||
|
{
|
||||||
|
public double Radius;
|
||||||
|
|
||||||
|
public Circle(double radius)
|
||||||
|
{
|
||||||
|
Radius = radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double GetArea()
|
||||||
|
{
|
||||||
|
return 3.14 * Radius * Radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
class Circle : Shape
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
`Circle` 是普通类,但没有实现 `Shape` 要求的 `GetArea()`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 子类也可以继续抽象
|
||||||
|
如果子类暂时不想实现所有抽象方法,那么这个子类也必须声明成 `abstract`。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Shape
|
||||||
|
{
|
||||||
|
public abstract double GetArea();
|
||||||
|
public abstract double GetLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class RoundShape : Shape
|
||||||
|
{
|
||||||
|
public override double GetLength()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArea 暂时不实现,所以 RoundShape 也必须是抽象类
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
普通类必须把抽象成员补完整
|
||||||
|
抽象类可以继续把没补完的内容留给下一级子类
|
||||||
|
|
||||||
|
----
|
||||||
|
# 抽象类里可以有什么
|
||||||
|
抽象类不只是能写抽象方法,也可以写普通成员。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Character
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
public int Hp;
|
||||||
|
|
||||||
|
public Character(string name, int hp)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Hp = hp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeDamage(int damage)
|
||||||
|
{
|
||||||
|
Hp -= damage;
|
||||||
|
Console.WriteLine(Name + "受到了" + damage + "点伤害");
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "正在移动");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Attack();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象类中可以有:
|
||||||
|
1. 成员变量
|
||||||
|
2. 成员属性
|
||||||
|
3. 构造函数
|
||||||
|
4. 普通方法
|
||||||
|
5. 虚方法
|
||||||
|
6. 抽象方法
|
||||||
|
7. 抽象属性、抽象索引器、抽象事件
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
抽象类可以有“已经写好的公共功能”,也可以有“要求子类必须补上的功能”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 抽象类可以有构造函数
|
||||||
|
抽象类不能直接 `new`,但它仍然可以有构造函数。
|
||||||
|
|
||||||
|
原因:
|
||||||
|
创建子类对象时,会先构造父类那一部分
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Enemy
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Enemy(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Console.WriteLine("Enemy构造");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Boss : Enemy
|
||||||
|
{
|
||||||
|
public Boss(string name) : base(name)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Boss构造");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "发动攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Boss boss = new Boss("魔王");
|
||||||
|
```
|
||||||
|
|
||||||
|
执行顺序还是:
|
||||||
|
1. 先执行父类 `Enemy` 构造函数
|
||||||
|
2. 再执行子类 `Boss` 构造函数
|
||||||
|
|
||||||
|
----
|
||||||
|
# abstract、virtual、override 的关系
|
||||||
|
`abstract` 方法:
|
||||||
|
```csharp
|
||||||
|
public abstract void Attack();
|
||||||
|
```
|
||||||
|
|
||||||
|
特点:
|
||||||
|
1. 没有方法体
|
||||||
|
2. 子类必须实现
|
||||||
|
3. 本身就表示允许被重写,所以子类用 `override`
|
||||||
|
|
||||||
|
`virtual` 方法:
|
||||||
|
```csharp
|
||||||
|
public virtual void Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine("默认移动");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
特点:
|
||||||
|
1. 有方法体
|
||||||
|
2. 子类可以选择重写,也可以不重写
|
||||||
|
3. 子类重写时也用 `override`
|
||||||
|
|
||||||
|
对比:
|
||||||
|
1. `abstract`:父类不给默认实现,子类必须改
|
||||||
|
2. `virtual`:父类给默认实现,子类可以改
|
||||||
|
3. `override`:子类真正改写父类的抽象方法或虚方法
|
||||||
|
|
||||||
|
----
|
||||||
|
# 抽象方法和普通方法的区别
|
||||||
|
普通方法:
|
||||||
|
```csharp
|
||||||
|
public void Eat()
|
||||||
|
{
|
||||||
|
Console.WriteLine("吃东西");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象方法:
|
||||||
|
```csharp
|
||||||
|
public abstract void Speak();
|
||||||
|
```
|
||||||
|
|
||||||
|
区别:
|
||||||
|
1. 普通方法有方法体,抽象方法没有方法体
|
||||||
|
2. 普通方法可以直接继承使用,抽象方法必须由子类实现
|
||||||
|
3. 普通方法可以写在普通类或抽象类里,抽象方法只能写在抽象类里
|
||||||
|
4. 抽象方法天然就是给多态准备的
|
||||||
|
|
||||||
|
----
|
||||||
|
# 抽象属性
|
||||||
|
抽象的不一定只有方法,也可以是属性。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Weapon
|
||||||
|
{
|
||||||
|
public abstract int Damage { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sword : Weapon
|
||||||
|
{
|
||||||
|
public override int Damage
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
简写:
|
||||||
|
```csharp
|
||||||
|
class Bow : Weapon
|
||||||
|
{
|
||||||
|
public override int Damage => 8;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
父类规定“武器必须有伤害值”
|
||||||
|
不同武器自己决定伤害值是多少
|
||||||
|
|
||||||
|
----
|
||||||
|
# 抽象类和密封类不能同时用
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
sealed abstract class Animal
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
1. `abstract` 表示这个类必须被继承后才能补完整
|
||||||
|
2. `sealed` 表示这个类不能被继承
|
||||||
|
3. 两个要求互相矛盾
|
||||||
|
|
||||||
|
这个点可以和[[密封类]]一起记。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见错误
|
||||||
|
错误1:抽象方法写了方法体
|
||||||
|
```csharp
|
||||||
|
abstract class Animal
|
||||||
|
{
|
||||||
|
public abstract void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("动物叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象方法不能有方法体。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误2:普通类里写抽象方法
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public abstract void Speak();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象方法只能写在抽象类中。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误3:子类没实现抽象方法
|
||||||
|
```csharp
|
||||||
|
abstract class Animal
|
||||||
|
{
|
||||||
|
public abstract void Speak();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Dog` 如果是普通类,就必须 `override Speak()`。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误4:抽象方法上再写 `virtual`
|
||||||
|
```csharp
|
||||||
|
abstract class Animal
|
||||||
|
{
|
||||||
|
public abstract virtual void Speak();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象方法本身就表示必须被重写,不需要再写 `virtual`。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误5:直接 new 抽象类
|
||||||
|
```csharp
|
||||||
|
Animal animal = new Animal();
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象类不能直接实例化。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 什么时候用抽象类
|
||||||
|
适合用抽象类的情况:
|
||||||
|
1. 多个子类确实属于同一种父类
|
||||||
|
2. 父类有公共字段、属性、构造函数或普通方法可以复用
|
||||||
|
3. 父类还需要强制子类实现某些行为
|
||||||
|
4. 你希望调用方通过父类统一调用子类对象
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- `Animal`:动物都能吃,但不同动物叫声不同
|
||||||
|
- `Shape`:图形都有面积,但不同图形面积算法不同
|
||||||
|
- `Character`:角色都有血量,但不同角色攻击方式不同
|
||||||
|
|
||||||
|
不适合用抽象类的情况:
|
||||||
|
1. 只是想规定一种能力,例如“能飞”“能攻击”
|
||||||
|
2. 多个类之间没有明确的父子关系
|
||||||
|
3. 一个类需要同时拥有多种能力
|
||||||
|
|
||||||
|
这些情况后面通常更适合用[[接口]]。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和多态的关系
|
||||||
|
抽象类经常和多态一起使用。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Skill
|
||||||
|
{
|
||||||
|
public abstract void Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FireSkill : Skill
|
||||||
|
{
|
||||||
|
public override void Release()
|
||||||
|
{
|
||||||
|
Console.WriteLine("释放火球");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IceSkill : Skill
|
||||||
|
{
|
||||||
|
public override void Release()
|
||||||
|
{
|
||||||
|
Console.WriteLine("释放冰锥");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UseSkill(Skill skill)
|
||||||
|
{
|
||||||
|
skill.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
UseSkill(new FireSkill());
|
||||||
|
UseSkill(new IceSkill());
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `Skill` 规定所有技能都必须能 `Release()`
|
||||||
|
2. `FireSkill` 和 `IceSkill` 各自实现释放逻辑
|
||||||
|
3. `UseSkill` 只依赖抽象父类,不依赖具体技能类
|
||||||
|
|
||||||
|
这就是:
|
||||||
|
==抽象类定规则,多态负责运行时执行不同实现==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. 抽象类不能直接创建对象
|
||||||
|
2. 抽象方法没有方法体,结尾用分号
|
||||||
|
3. 有抽象方法的类必须是抽象类
|
||||||
|
4. 普通子类继承抽象类后,必须实现所有抽象成员
|
||||||
|
5. 抽象类可以有普通方法、字段、属性和构造函数
|
||||||
|
6. 抽象方法不能写 `private`,因为子类需要能重写它
|
||||||
|
7. 抽象方法不能同时写 `virtual`,因为它本身就是虚的
|
||||||
|
8. 抽象类不能和 `sealed` 同时使用
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==抽象类不能直接 new,抽象方法只定规则不写实现,子类必须 override。==
|
||||||
|
|
||||||
|
再压缩一点:
|
||||||
|
==父类抽象定规矩,子类重写补细节。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“抽象类是用 `abstract` 修饰的类,不能直接实例化,主要用来作为父类。
|
||||||
|
它可以包含普通成员,也可以包含抽象成员。抽象方法没有方法体,只声明方法签名,普通子类继承后必须用 `override` 实现。
|
||||||
|
抽象类适合用来提取多个子类的共同代码,同时强制子类实现不同的行为,常和继承、多态一起使用。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [abstract (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract)
|
||||||
|
2. Microsoft Learn: [Abstract and Sealed Classes and Class Members](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/abstract-and-sealed-classes-and-class-members)
|
||||||
|
3. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
|
||||||
|
|
||||||
|
#抽象类
|
||||||
|
#抽象方法
|
||||||
|
#核心
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
# 抽象类和接口的区别
|
||||||
|
概念:
|
||||||
|
抽象类和接口都可以用来做抽象,都能配合多态使用
|
||||||
|
但它们表达的语义不一样
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
抽象类更像“是什么”
|
||||||
|
接口更像“能做什么”
|
||||||
|
|
||||||
|
例如:
|
||||||
|
1. `Animal` 抽象类:表示狗、猫、鸟都是动物
|
||||||
|
2. `IFlyable` 接口:表示某个对象具备飞行能力
|
||||||
|
|
||||||
|
----
|
||||||
|
# 先看抽象类
|
||||||
|
抽象类:
|
||||||
|
```csharp
|
||||||
|
abstract class Animal
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Animal(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Eat()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "吃东西");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Speak();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
子类:
|
||||||
|
```csharp
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public Dog(string name) : base(name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "汪汪");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象类适合:
|
||||||
|
1. 有明确父子关系
|
||||||
|
2. 要复用公共字段、构造函数、普通方法
|
||||||
|
3. 还要强制子类实现某些行为
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
抽象类适合表达“这类东西的共同基础”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 再看接口
|
||||||
|
接口:
|
||||||
|
```csharp
|
||||||
|
interface IFlyable
|
||||||
|
{
|
||||||
|
void Fly();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现类:
|
||||||
|
```csharp
|
||||||
|
class Bird : IFlyable
|
||||||
|
{
|
||||||
|
public void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("鸟飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Plane : IFlyable
|
||||||
|
{
|
||||||
|
public void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("飞机飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
接口适合:
|
||||||
|
1. 表达一种能力
|
||||||
|
2. 多个无关类型都能实现同一种行为
|
||||||
|
3. 一个类需要同时拥有多种能力
|
||||||
|
4. 调用方只关心能力,不关心具体身份
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
接口适合表达“谁能做这件事”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 核心区别
|
||||||
|
| 对比点 | 抽象类 | 接口 |
|
||||||
|
|---|---|---|
|
||||||
|
| 关键词 | `abstract class` | `interface` |
|
||||||
|
| 主要表达 | 是什么 | 能做什么 |
|
||||||
|
| 继承数量 | 一个类只能继承一个类 | 一个类可以实现多个接口 |
|
||||||
|
| 成员实现 | 可以有普通实现,也可以有抽象成员 | 主要规定契约,要求实现类提供成员 |
|
||||||
|
| 字段 | 可以有字段 | 通常不放实例字段 |
|
||||||
|
| 构造函数 | 可以有构造函数 | 不能像类一样写实例构造函数 |
|
||||||
|
| 适合场景 | 提取共同父类和公共代码 | 表达能力、规则、插件式扩展 |
|
||||||
|
| 多态使用 | 可以用抽象类引用指向子类 | 可以用接口引用指向实现类 |
|
||||||
|
|
||||||
|
----
|
||||||
|
# 单继承和多实现
|
||||||
|
C# 中:
|
||||||
|
一个类只能继承一个父类
|
||||||
|
但可以实现多个接口
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Character
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMovable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttackable
|
||||||
|
{
|
||||||
|
void Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player : Character, IMovable, IAttackable
|
||||||
|
{
|
||||||
|
public void Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine("移动");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `Player` 是一种 `Character`
|
||||||
|
2. `Player` 同时具备移动能力和攻击能力
|
||||||
|
3. 父类写一个,接口可以写多个
|
||||||
|
|
||||||
|
----
|
||||||
|
# 什么时候用抽象类
|
||||||
|
适合用抽象类:
|
||||||
|
```csharp
|
||||||
|
abstract class Enemy
|
||||||
|
{
|
||||||
|
public int Hp;
|
||||||
|
|
||||||
|
public void TakeDamage(int damage)
|
||||||
|
{
|
||||||
|
Hp -= damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Attack();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因为:
|
||||||
|
1. 小怪、Boss、远程敌人都可以算敌人
|
||||||
|
2. 它们都有血量、受伤逻辑
|
||||||
|
3. 它们攻击方式不同
|
||||||
|
|
||||||
|
这种情况抽象类很合适:
|
||||||
|
有共同身份,也有公共代码
|
||||||
|
|
||||||
|
----
|
||||||
|
# 什么时候用接口
|
||||||
|
适合用接口:
|
||||||
|
```csharp
|
||||||
|
interface IDamageable
|
||||||
|
{
|
||||||
|
void TakeDamage(int damage);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现者可能是:
|
||||||
|
```csharp
|
||||||
|
class Player : IDamageable
|
||||||
|
{
|
||||||
|
public void TakeDamage(int damage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Enemy : IDamageable
|
||||||
|
{
|
||||||
|
public void TakeDamage(int damage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Box : IDamageable
|
||||||
|
{
|
||||||
|
public void TakeDamage(int damage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
玩家、敌人、箱子不是同一种父类关系,但它们都可以“受伤/受击”。
|
||||||
|
|
||||||
|
这种情况接口更合适:
|
||||||
|
没有共同身份,但有共同能力
|
||||||
|
|
||||||
|
----
|
||||||
|
# 可以一起用
|
||||||
|
抽象类和接口不是二选一,经常会一起使用。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
abstract class Character
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Character(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttackable
|
||||||
|
{
|
||||||
|
void Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Warrior : Character, IAttackable
|
||||||
|
{
|
||||||
|
public Warrior(string name) : base(name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "更新状态");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "挥剑攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `Warrior` 是一种 `Character`
|
||||||
|
2. `Warrior` 具备 `IAttackable` 能力
|
||||||
|
3. 抽象类负责共同身份和基础逻辑
|
||||||
|
4. 接口负责额外能力
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和多态的关系
|
||||||
|
抽象类多态:
|
||||||
|
```csharp
|
||||||
|
Animal animal = new Dog();
|
||||||
|
animal.Speak();
|
||||||
|
```
|
||||||
|
|
||||||
|
接口多态:
|
||||||
|
```csharp
|
||||||
|
IFlyable flyable = new Bird();
|
||||||
|
flyable.Fly();
|
||||||
|
```
|
||||||
|
|
||||||
|
它们都能做到:
|
||||||
|
用统一类型接收不同具体对象
|
||||||
|
运行时执行不同实现
|
||||||
|
|
||||||
|
区别在于:
|
||||||
|
1. 抽象类变量强调“它属于某个父类体系”
|
||||||
|
2. 接口变量强调“它拥有某个能力”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见判断方法
|
||||||
|
问自己几个问题:
|
||||||
|
|
||||||
|
1. 这些类是不是同一种东西?
|
||||||
|
- 是:优先考虑抽象类
|
||||||
|
- 不是:优先考虑接口
|
||||||
|
2. 是否需要复用字段、构造函数、普通方法?
|
||||||
|
- 是:抽象类更合适
|
||||||
|
3. 一个类是否需要同时拥有多种能力?
|
||||||
|
- 是:接口更合适
|
||||||
|
4. 调用方真正关心的是身份还是能力?
|
||||||
|
- 关心身份:抽象类
|
||||||
|
- 关心能力:接口
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见误区
|
||||||
|
误区1:接口就是更高级的抽象类
|
||||||
|
|
||||||
|
不是。
|
||||||
|
接口和抽象类不是上下级关系,而是用途不同
|
||||||
|
|
||||||
|
----
|
||||||
|
误区2:有公共方法就一定用抽象类
|
||||||
|
|
||||||
|
不一定。
|
||||||
|
如果这些类型没有真正的父子关系,硬抽象成父类可能违反[[里氏替换原则]]
|
||||||
|
|
||||||
|
----
|
||||||
|
误区3:接口只能有方法
|
||||||
|
|
||||||
|
接口也可以有属性、索引器、事件等成员。
|
||||||
|
初学阶段先重点掌握方法和属性即可。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. 抽象类和接口都不能直接创建对象
|
||||||
|
2. 抽象类用 `abstract class`,接口用 `interface`
|
||||||
|
3. 抽象类适合表达共同身份和公共代码
|
||||||
|
4. 接口适合表达能力和契约
|
||||||
|
5. 类只能继承一个父类,但可以实现多个接口
|
||||||
|
6. 抽象类和接口都可以配合多态
|
||||||
|
7. 设计时先看语义,不要只看语法
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==抽象类管“是谁的一种”,接口管“能做什么”。==
|
||||||
|
|
||||||
|
再压缩一点:
|
||||||
|
==同类用抽象类,能力用接口。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“抽象类和接口都能表达抽象,也都可以配合多态使用。
|
||||||
|
抽象类更强调父子关系和公共代码复用,一个类只能继承一个抽象类;接口更强调能力或契约,一个类可以实现多个接口。
|
||||||
|
如果多个类本质上是同一种东西,并且有公共字段、构造函数或普通方法,可以用抽象类;如果只是多个类型都有同一种能力,通常用接口更合适。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [abstract (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract)
|
||||||
|
2. Microsoft Learn: [interface (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface)
|
||||||
|
3. Microsoft Learn: [Interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces)
|
||||||
|
|
||||||
|
#抽象类
|
||||||
|
#接口
|
||||||
|
#核心
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
# 接口
|
||||||
|
概念:
|
||||||
|
接口使用 `interface` 关键字定义
|
||||||
|
它主要用来规定一组==能力 / 规则 / 契约==
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
接口不关心“你是谁”
|
||||||
|
只关心“你能不能做这件事”
|
||||||
|
|
||||||
|
例如:
|
||||||
|
`IFlyable` 表示“会飞”
|
||||||
|
不管你是鸟、飞机还是飞行道具,只要实现了这个接口,就代表你拥有飞行能力
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 规定某些类必须实现哪些成员
|
||||||
|
2. 表达一种能力,例如会飞、会攻击、可保存、可移动
|
||||||
|
3. 让不同类型用同一套调用方式
|
||||||
|
4. 弥补 C# 类只能单继承的问题,因为一个类可以实现多个接口
|
||||||
|
5. 配合[[多态vob|多态]],让代码面向能力编程
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
定义接口:
|
||||||
|
```csharp
|
||||||
|
interface 接口名
|
||||||
|
{
|
||||||
|
void 方法名();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现接口:
|
||||||
|
```csharp
|
||||||
|
class 类名 : 接口名
|
||||||
|
{
|
||||||
|
public void 方法名()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
命名习惯:
|
||||||
|
接口名通常以大写字母 `I` 开头
|
||||||
|
例如:`IFlyable`、`IAttackable`、`ISaveable`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
interface IFlyable
|
||||||
|
{
|
||||||
|
void Fly();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bird : IFlyable
|
||||||
|
{
|
||||||
|
public void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("鸟在飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Plane : IFlyable
|
||||||
|
{
|
||||||
|
public void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("飞机在飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LetItFly(IFlyable flyable)
|
||||||
|
{
|
||||||
|
flyable.Fly();
|
||||||
|
}
|
||||||
|
|
||||||
|
LetItFly(new Bird());
|
||||||
|
LetItFly(new Plane());
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `IFlyable` 规定实现者必须有 `Fly()` 方法
|
||||||
|
2. `Bird` 和 `Plane` 类型不同,但都实现了 `IFlyable`
|
||||||
|
3. `LetItFly` 不关心传进来的是鸟还是飞机,只关心它会不会飞
|
||||||
|
|
||||||
|
这就是接口常见用法:
|
||||||
|
==面向能力编程,而不是面向具体类型编程==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 接口中的成员
|
||||||
|
常见接口成员:
|
||||||
|
```csharp
|
||||||
|
interface IMovable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
int Speed { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现:
|
||||||
|
```csharp
|
||||||
|
class Player : IMovable
|
||||||
|
{
|
||||||
|
public int Speed { get; set; }
|
||||||
|
|
||||||
|
public void Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine("玩家移动");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
接口中常见成员包括:
|
||||||
|
1. 方法
|
||||||
|
2. 属性
|
||||||
|
3. 索引器
|
||||||
|
4. 事件
|
||||||
|
|
||||||
|
初学阶段重点先掌握:
|
||||||
|
接口方法 + 接口属性
|
||||||
|
|
||||||
|
----
|
||||||
|
# 接口成员默认是 public
|
||||||
|
接口里这样写:
|
||||||
|
```csharp
|
||||||
|
interface IAttackable
|
||||||
|
{
|
||||||
|
void Attack();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现时必须写 `public`:
|
||||||
|
```csharp
|
||||||
|
class Enemy : IAttackable
|
||||||
|
{
|
||||||
|
public void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("敌人攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
class Enemy : IAttackable
|
||||||
|
{
|
||||||
|
void Attack()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
接口成员代表对外公开的契约
|
||||||
|
类实现接口时,成员访问级别不能比接口更低
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一个类可以实现多个接口
|
||||||
|
C# 的类只能继承一个父类,但可以实现多个接口。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
interface IMovable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttackable
|
||||||
|
{
|
||||||
|
void Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player : IMovable, IAttackable
|
||||||
|
{
|
||||||
|
public void Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine("玩家移动");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("玩家攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
`Player` 同时拥有“可移动”和“可攻击”两种能力
|
||||||
|
|
||||||
|
----
|
||||||
|
# 继承类和实现接口同时存在
|
||||||
|
写法:
|
||||||
|
```csharp
|
||||||
|
class 子类名 : 父类名, 接口1, 接口2
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意顺序:
|
||||||
|
==先写父类,再写接口==
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
class Character
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttackable
|
||||||
|
{
|
||||||
|
void Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Warrior : Character, IAttackable
|
||||||
|
{
|
||||||
|
public void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine(Name + "挥剑攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
错误示意:
|
||||||
|
```csharp
|
||||||
|
// class Warrior : IAttackable, Character
|
||||||
|
// {
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
如果既要继承类,又要实现接口,父类必须写在最前面。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 接口也可以继承接口
|
||||||
|
接口之间可以继承,而且可以继承多个接口。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
interface IMovable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttackable
|
||||||
|
{
|
||||||
|
void Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPlayerAction : IMovable, IAttackable
|
||||||
|
{
|
||||||
|
void UseItem();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现 `IPlayerAction` 的类,必须实现:
|
||||||
|
1. `Move()`
|
||||||
|
2. `Attack()`
|
||||||
|
3. `UseItem()`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Player : IPlayerAction
|
||||||
|
{
|
||||||
|
public void Move()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Attack()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UseItem()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 接口引用和多态
|
||||||
|
接口也可以当变量类型使用。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IAttackable attacker = new Player();
|
||||||
|
attacker.Attack();
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以作为方法参数:
|
||||||
|
```csharp
|
||||||
|
void DoAttack(IAttackable attacker)
|
||||||
|
{
|
||||||
|
attacker.Attack();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
好处:
|
||||||
|
调用方只依赖 `IAttackable`
|
||||||
|
任何实现了这个接口的对象都能传进来
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
DoAttack(new Player());
|
||||||
|
DoAttack(new Enemy());
|
||||||
|
```
|
||||||
|
|
||||||
|
这也是多态的一种表现。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 接口和里氏替换原则
|
||||||
|
接口常用来拆分能力,避免不合理继承。
|
||||||
|
|
||||||
|
比如:
|
||||||
|
```csharp
|
||||||
|
abstract class Bird
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFlyable
|
||||||
|
{
|
||||||
|
void Fly();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sparrow : Bird, IFlyable
|
||||||
|
{
|
||||||
|
public void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("麻雀飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Penguin : Bird
|
||||||
|
{
|
||||||
|
public void Swim()
|
||||||
|
{
|
||||||
|
Console.WriteLine("企鹅游泳");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样:
|
||||||
|
1. `Bird` 只表达“鸟”
|
||||||
|
2. `IFlyable` 只表达“会飞”
|
||||||
|
3. 不会飞的企鹅不用硬实现 `Fly()`
|
||||||
|
|
||||||
|
这个例子可以和[[里氏替换原则]]一起理解。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 显式实现接口
|
||||||
|
当两个接口有同名方法,或者你不希望方法直接暴露在类对象上,可以显式实现接口。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
interface IWalkable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFlyable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bird : IWalkable, IFlyable
|
||||||
|
{
|
||||||
|
void IWalkable.Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine("鸟在走");
|
||||||
|
}
|
||||||
|
|
||||||
|
void IFlyable.Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine("鸟在飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bird bird = new Bird();
|
||||||
|
|
||||||
|
IWalkable walkable = bird;
|
||||||
|
walkable.Move();
|
||||||
|
|
||||||
|
IFlyable flyable = bird;
|
||||||
|
flyable.Move();
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
显式实现接口时,不写 `public`
|
||||||
|
调用时通常要先把对象当成对应接口类型
|
||||||
|
|
||||||
|
初学阶段先知道有这个写法即可,最常用的还是普通实现接口。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见错误
|
||||||
|
错误1:实现接口时忘记 `public`
|
||||||
|
```csharp
|
||||||
|
class Player : IAttackable
|
||||||
|
{
|
||||||
|
void Attack()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
接口成员默认是公开契约,实现时通常要写 `public`。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误2:没有实现接口所有成员
|
||||||
|
```csharp
|
||||||
|
interface IMovable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
int Speed { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player : IMovable
|
||||||
|
{
|
||||||
|
public void Move()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Speed` 没实现,所以会报错。
|
||||||
|
|
||||||
|
----
|
||||||
|
错误3:把接口当成类直接创建对象
|
||||||
|
```csharp
|
||||||
|
IFlyable flyable = new IFlyable();
|
||||||
|
```
|
||||||
|
|
||||||
|
接口只是规则,不能直接 `new`。
|
||||||
|
|
||||||
|
正确写法:
|
||||||
|
```csharp
|
||||||
|
IFlyable flyable = new Bird();
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 什么时候用接口
|
||||||
|
适合用接口的情况:
|
||||||
|
1. 你要表达的是“能力”,不是“父子身份”
|
||||||
|
2. 多个无关类型都有同一种行为
|
||||||
|
3. 一个类型需要拥有多种能力
|
||||||
|
4. 想让代码依赖规则,而不是依赖具体类
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- `IFlyable`:会飞
|
||||||
|
- `IAttackable`:能攻击
|
||||||
|
- `ISaveable`:可保存
|
||||||
|
- `IDamageable`:可受伤
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. 接口用 `interface` 定义
|
||||||
|
2. 接口名通常以 `I` 开头
|
||||||
|
3. 接口不能直接创建对象
|
||||||
|
4. 类和结构体都可以实现接口
|
||||||
|
5. 一个类可以实现多个接口
|
||||||
|
6. 接口成员实现时通常要写 `public`
|
||||||
|
7. 接口更适合表达“能做什么”,抽象类更适合表达“是什么”
|
||||||
|
8. 接口常和多态、里氏替换原则一起理解
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==接口是能力契约,谁实现它,谁就必须按规则提供这些能力。==
|
||||||
|
|
||||||
|
再压缩一点:
|
||||||
|
==接口管能不能做,不管你是谁。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“接口是用 `interface` 定义的一组契约,主要用来规定实现类必须提供哪些成员。
|
||||||
|
类可以实现多个接口,所以接口常用来表达能力,比如可移动、可攻击、可保存。
|
||||||
|
调用方可以面向接口编程,只依赖能力规则,不依赖具体类,这样更利于扩展和多态。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [interface (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface)
|
||||||
|
2. Microsoft Learn: [Interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces)
|
||||||
|
|
||||||
|
#接口
|
||||||
|
#核心
|
||||||
@@ -142,4 +142,8 @@ step4:D放入
|
|||||||
![[Pasted image 20260412210946.png]]
|
![[Pasted image 20260412210946.png]]
|
||||||
step5:如果0代1代后续又满了,那么这套算法会同时做用在两代内存中,以此类推
|
step5:如果0代1代后续又满了,那么这套算法会同时做用在两代内存中,以此类推
|
||||||
类似多级缓存cache,L1最快但常换L3最慢
|
类似多级缓存cache,L1最快但常换L3最慢
|
||||||
#基础
|
<<<<<<< ours
|
||||||
|
#基础
|
||||||
|
=======
|
||||||
|
#基础
|
||||||
|
>>>>>>> theirs
|
||||||
|
|||||||
+123
-188
@@ -1,245 +1,180 @@
|
|||||||
# 构造函数:
|
# 构造函数
|
||||||
概念:
|
概念:
|
||||||
实例化时,用于初始化的函数
|
- 构造函数用于在对象创建时做初始化
|
||||||
不写默认有一个无参构造函数
|
- 构造函数名必须和类名相同
|
||||||
==可以重载==
|
- 构造函数没有返回值类型
|
||||||
|
- ==可以重载==
|
||||||
|
|
||||||
通常用法:
|
通常用法:
|
||||||
自定义初始化规则
|
- 给字段或属性设置初始值
|
||||||
|
- 保证对象一创建出来就处于可用状态
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
访问修饰符 类名(){}
|
```csharp
|
||||||
|
访问修饰符 类名(参数列表)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
```Csharp
|
```csharp
|
||||||
class Person()
|
class Person
|
||||||
{
|
{
|
||||||
public string name;
|
public string Name;
|
||||||
public int age;
|
public int Age;
|
||||||
|
|
||||||
public Person()
|
public Person()
|
||||||
{
|
{
|
||||||
name = "默认值";
|
Name = "默认值";
|
||||||
age = 默认值;
|
Age = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Person(string name, int age)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Age = age;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
this关键字:
|
`this` 调用其他构造函数:
|
||||||
this指当前类声明的成员变量
|
```csharp
|
||||||
```Csharp
|
class Person
|
||||||
class Person()
|
{
|
||||||
|
public string Name;
|
||||||
|
public int Age;
|
||||||
|
|
||||||
|
public Person() : this("默认值", 0)
|
||||||
{
|
{
|
||||||
public string name;
|
|
||||||
public int age;
|
|
||||||
public Person()
|
|
||||||
{
|
|
||||||
name = "默认值";
|
|
||||||
age = 默认值;
|
|
||||||
}
|
|
||||||
public Person(int age,string name) //重构
|
|
||||||
{
|
|
||||||
this.name = name;
|
|
||||||
//后面的name是传递的参数,this.name是最上方public int出来的
|
|
||||||
this.age = name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Person(string name, int age)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Age = age;
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
特殊写法:
|
|
||||||
通过this重用构造函数代码,即先调用无参构造函数,然后在调用这个特殊写法的构造函数
|
|
||||||
语法:访问修饰符 构造函数名(参数列表):this(【可选】参数1,参数2.....)
|
|
||||||
例子:
|
|
||||||
```Csharp
|
|
||||||
public Person(int age, string name):this(age)
|
|
||||||
{
|
|
||||||
balabala
|
|
||||||
}
|
|
||||||
```
|
|
||||||
过程:先调用无参Person构造函数,然后寻找只有构造单个参数age的构造函数进行初始化
|
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
==函数名和类名必须相同==
|
1. ==如果类里完全没写任何实例构造函数,编译器才会自动补一个无参构造函数==
|
||||||
==没有特殊用途,一般都是public==
|
2. ==只要你自己写了任意实例构造函数,默认无参构造函数就不会再自动生成==
|
||||||
|
3. 构造函数通常写 `public`,但也可以根据设计写成 `private` / `protected`
|
||||||
==如果写了有参构造函数且自己没有保留无参构造函数,那么类在实例化时只能使用有参构造函数且无法使用无参构造函数==
|
4. 子类构造函数可以通过 `base(...)` 调用父类构造函数
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
# 析构函数
|
# 析构函数
|
||||||
概念:
|
概念:
|
||||||
当引用类型堆内存被回收时,会调用该函数
|
- C# 里的析构函数更准确地说是==终结器(finalizer)==
|
||||||
对于需要手动管理内存的语言(C++),需要在析构函数中做一些内存回收处理
|
- 当对象即将被 GC 回收时,运行时可能会调用它
|
||||||
C#有自动垃圾回收机制GC
|
- 它主要用于“兜底清理非托管资源”
|
||||||
Unity中基本不用管
|
|
||||||
|
|
||||||
通常用法:
|
|
||||||
|
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
```Csharp
|
```csharp
|
||||||
~类名()
|
class Person
|
||||||
{
|
{
|
||||||
}
|
~Person()
|
||||||
```
|
{
|
||||||
案例:
|
// 清理代码
|
||||||
```Csharp
|
}
|
||||||
~Person()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
|
1. 析构函数只能写在==类==里,结构体不能写
|
||||||
|
2. 不能手动直接调用析构函数
|
||||||
|
3. 析构函数执行时机==不确定==
|
||||||
|
4. 大多数业务代码里==不推荐为了练习而滥写析构函数==
|
||||||
|
5. 现代 .NET 中,更常见的做法是优先使用 `IDisposable`、`using` 和 `SafeHandle`
|
||||||
|
|
||||||
----
|
----
|
||||||
# 垃圾回收
|
# 垃圾回收
|
||||||
垃圾回收(GC)
|
垃圾回收(GC):
|
||||||
通过遍历堆Heap上动态分配的所有对象
|
- GC 是 .NET 的自动内存管理机制
|
||||||
通过识别他们是否被引用来确定哪些对象时垃圾,哪些对象仍然需要引用
|
- 它负责识别哪些托管对象还“活着”,哪些已经不可达,可以回收
|
||||||
垃圾:没有变量或对象引用的内容
|
|
||||||
如果是垃圾就要被回收释放
|
|
||||||
|
|
||||||
一般都是==被动触发(会造成卡顿)==,所以游戏一般需要手动执行垃圾回收
|
基础理解:
|
||||||
语法:GC.Collect();
|
1. 只要对象还能从“根”访问到,就一般不会被回收
|
||||||
==GC会造成卡顿,所以在加载Loading时一般会调用==
|
2. 访问不到的对象,才有机会被 GC 回收
|
||||||
|
3. GC 主要管理的是==托管堆上的对象==
|
||||||
|
|
||||||
垃圾回收有多种算法
|
分代概念:
|
||||||
引用计数
|
- .NET GC 常分为 0、1、2 代
|
||||||
标记清楚
|
- 新对象一般先进入 0 代
|
||||||
标记整理
|
- 活得越久,越可能被提升到更高代
|
||||||
复制集合
|
- 大对象通常会进入 LOH(大对象堆),阈值常见记法是 >= `85,000` 字节
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
GC只负责Heap内存的垃圾祸首
|
1. ==`GC.Collect()` 一般不建议手动乱调==
|
||||||
引用类型都是在Heap中的,所以他需要GC来进行管理
|
2. 手动触发 GC 可能带来性能抖动
|
||||||
|
3. “值类型一定在栈上、引用类型一定在堆上”不是绝对规则
|
||||||
|
4. GC 解决的是==托管内存==问题,不等于所有资源都会自动及时释放
|
||||||
|
|
||||||
栈Stack上的内存时系统自动管理的
|
----
|
||||||
所以不需要GC
|
# GC 和 IDisposable
|
||||||
|
首先区分资源类型:
|
||||||
|
|
||||||
C#中垃圾回收机制的大概原理
|
- **托管资源**
|
||||||
他会把内存分为0,1,2代内存
|
比如普通对象、字符串、集合。主要由 GC 管理。
|
||||||
代的概念是垃圾回收中的一种算法需要
|
- **非托管资源**
|
||||||
新分配的对象都会被配置在第0代内存中
|
比如文件句柄、数据库连接、Socket、窗口句柄。需要更及时地手动释放。
|
||||||
每次分配都可能会进行垃圾回收(一般来说0代内存满了就会触发)
|
|
||||||
在第一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行一下两部
|
|
||||||
1.标记对象,从根(静态参数、方法参数)开始检查引用对象,标记后为可达对象,未标记就是不可大对象
|
|
||||||
不可达对象就会认为是垃圾
|
|
||||||
2.搬迁对象 压缩堆 (挂起执行内存托管代码线程:把第0代内存(还不是垃圾的内存)放到一代进行搬迁)释放未标记的对象、搬迁可达对象、修改引用地址
|
|
||||||
|
|
||||||
大对象总被认为是二带内存,目的是减少性能损耗
|
为什么要用 `IDisposable`:
|
||||||
不会对大对象进行压缩搬迁 大对象是85000kb以上的
|
- 因为 GC 只负责“对象内存”回收
|
||||||
|
- 但像文件、连接、句柄这类资源,经常需要你主动关闭
|
||||||
|
|
||||||
示意图
|
最常见写法:
|
||||||
step1:程序放入内存
|
|
||||||
![[Pasted image 20260412210327.png|697]]
|
|
||||||
step2:发现沾满了
|
|
||||||
![[Pasted image 20260412210704.png]]
|
|
||||||
step3:原来的地址搬迁,无引用的垃圾丢掉
|
|
||||||
![[Pasted image 20260412210810.png]]
|
|
||||||
step4:D放入
|
|
||||||
![[Pasted image 20260412210946.png]]
|
|
||||||
step5:如果0代1代后续又满了,那么这套算法会同时做用在两代内存中,以此类推
|
|
||||||
类似多级缓存cache,L1最快但常换L3最慢
|
|
||||||
#基础
|
|
||||||
# GC和IDispose()
|
|
||||||
### 首先区分资源类型:
|
|
||||||
|
|
||||||
- **托管资源**:C# 自己管理的内存(比如普通对象、字符串)
|
|
||||||
- 不用你管,**垃圾回收器(GC)会自动释放**
|
|
||||||
- **非托管资源**:C# 管不了的外部资源
|
|
||||||
- 例子:文件流、数据库连接、网络套接字、图片句柄、系统内核对象
|
|
||||||
- **必须手动释放**,否则会造成**资源泄漏**(程序越跑越慢、卡死)
|
|
||||||
|
|
||||||
### 为什么要用 `IDisposable`?
|
|
||||||
|
|
||||||
- 非托管资源 GC 不会自动清理
|
|
||||||
- 必须提供一个**标准、统一**的方式手动释放
|
|
||||||
- `IDisposable` 就是 .NET 专门定义的**释放资源标准接口**
|
|
||||||
|
|
||||||
### 标准释放模式
|
|
||||||
例子:
|
|
||||||
```csharp
|
```csharp
|
||||||
using System;
|
using var stream = new FileStream("demo.txt", FileMode.OpenOrCreate);
|
||||||
|
// 使用 stream
|
||||||
|
```
|
||||||
|
|
||||||
// 1. 实现 IDisposable 接口
|
说明:
|
||||||
|
- `using` 结束时会自动调用 `Dispose()`
|
||||||
|
- 这比“等 GC 什么时候想起来再收”更可靠
|
||||||
|
|
||||||
|
标准释放模式示意:
|
||||||
|
```csharp
|
||||||
public class MyResource : IDisposable
|
public class MyResource : IDisposable
|
||||||
{
|
{
|
||||||
// 标记资源是否已释放
|
private bool _disposed;
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
// 2. 公共释放方法(外部调用)
|
public void Dispose()
|
||||||
public void Dispose()
|
{
|
||||||
{
|
Dispose(true);
|
||||||
Dispose(true);
|
GC.SuppressFinalize(this);
|
||||||
// 告诉GC:不用再管我了,我已经手动释放完了
|
}
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 核心释放逻辑
|
protected virtual void Dispose(bool disposing)
|
||||||
protected virtual void Dispose(bool disposing)
|
{
|
||||||
{
|
if (_disposed) return;
|
||||||
if (_disposed) return;
|
|
||||||
|
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
// 释放 托管资源
|
// 释放托管资源
|
||||||
}
|
}
|
||||||
|
|
||||||
// 释放 非托管资源(比如关闭文件、关闭数据库连接)
|
// 释放非托管资源
|
||||||
// 这里写你的资源释放代码
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
_disposed = true;
|
~MyResource()
|
||||||
}
|
{
|
||||||
|
Dispose(false);
|
||||||
// 4. 析构函数:防止用户忘记调用 Dispose
|
}
|
||||||
~MyResource()
|
|
||||||
{
|
|
||||||
Dispose(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
##### 解释:
|
|
||||||
逻辑:
|
|
||||||
1. 用完实例化对象
|
|
||||||
2. 调用`.Dispose()`手写的`.Close()`等方法进行手动释放
|
|
||||||
3. 判断标记变量
|
|
||||||
4. 没释放就执行释放代码
|
|
||||||
5. 标记为`true`
|
|
||||||
|
|
||||||
`GC.SuppressFinalize(this);` 的作用
|
补充理解:
|
||||||
告诉.NET的GC这个对象的非托管资源,我**手动释放完了**,你不需要在垃圾回收的时候,再执行它的析构函数了。==它不会取消GC运行,只是取消执行析构函数!==
|
1. `Dispose()` 是“我现在就主动释放”
|
||||||
|
2. 析构函数是“万一你忘了,我最后帮你兜底一次”
|
||||||
|
3. `GC.SuppressFinalize(this)` 的意思是:
|
||||||
|
这个对象已经手动释放好了,不用再走终结器那一套
|
||||||
析构函数 `~MyResource()` 是干嘛的?和 GC 啥关系?
|
|
||||||
防止你忘记调用 `Dispose()`
|
|
||||||
场景模拟
|
|
||||||
1. 你写了代码,**马虎忘了**调用 `.Dispose()`
|
|
||||||
2. 非托管资源(文件 / 数据库)一直占着没释放
|
|
||||||
3. 等到 GC(清洁工)来回收垃圾时
|
|
||||||
4. **自动触发析构函数** `~MyResource()`
|
|
||||||
5. 函数里执行 `Dispose(false)`,强行把非托管资源释放了
|
|
||||||
|
|
||||||
|
|
||||||
==一个实例化对象dispose后就不会再用了(设计上就是如此,用过dispose后标签一直是true)==
|
|
||||||
==而且由于它dispose了,内部引用是空的,所以只会留下一个实例化的小壳==
|
|
||||||
==实例化的小壳也会因为没有被引用被GC清理,不会造成太大的性能问题==
|
|
||||||
|
|
||||||
如果需要==一次性==彻底释放
|
|
||||||
例子:
|
|
||||||
```Csharp
|
|
||||||
var resource1 = new MyResource();
|
|
||||||
resource1.Dispose();
|
|
||||||
|
|
||||||
//手动切断引用
|
|
||||||
resource1 = null;
|
|
||||||
//手动触发GC
|
|
||||||
GC.Collection();
|
|
||||||
GC.WaitForPendingFinalizers();//等待回收完成
|
|
||||||
```
|
|
||||||
|
|
||||||
|
复习重点:
|
||||||
|
- GC 管托管内存
|
||||||
|
- `Dispose` 管需要及时释放的资源
|
||||||
|
- 最常用姿势不是手写 `GC.Collect()`,而是优先写好 `using` / `Dispose`
|
||||||
|
|
||||||
#封装
|
#封装
|
||||||
#核心
|
#核心
|
||||||
|
|||||||
+35
-16
@@ -1,36 +1,55 @@
|
|||||||
|
# 索引器
|
||||||
概念:
|
概念:
|
||||||
让对象像数组一样可以通过下标访问,方便程序编写
|
- 索引器(Indexer)可以让对象像数组一样通过 `[]` 访问
|
||||||
|
- 它很像属性,只是访问器带参数
|
||||||
|
- 一般用一个数组和索引器写法来实现按照下标访问成员
|
||||||
|
|
||||||
通常用法:
|
通常用法:
|
||||||
|
- 封装内部数组或集合
|
||||||
|
- 让类的使用方式更自然
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
```Csharp
|
```csharp
|
||||||
访问修饰符 返回值 this[参数类型 参数名1,参数类型 参数名2.....]
|
访问修饰符 返回值类型 this[参数列表]
|
||||||
{
|
{
|
||||||
get{} ==内部写法和属性相似==
|
get { }
|
||||||
set{}
|
set { }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
```Csharp
|
```csharp
|
||||||
class Person
|
class Team
|
||||||
{
|
{
|
||||||
private string name;
|
private string[] members = new string[3];
|
||||||
priavte int age;
|
|
||||||
private Person[] friends;
|
public string this[int index]
|
||||||
public Person this[int index]
|
|
||||||
{
|
{
|
||||||
get{return friends[index];}
|
get
|
||||||
set{friends[index] = value;}
|
{
|
||||||
|
return members[index];
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
members[index] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Person p1 = new Person();
|
Team team = new Team();
|
||||||
p[0] = new Person();
|
team[0] = "小明";
|
||||||
|
Console.WriteLine(team[0]);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
补充:
|
||||||
|
- 索引器也可以只有 `get`,做成只读
|
||||||
|
- 索引器也可以有多个参数,比如 `this[int row, int col]`
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
|
1. 索引器写法里的名字固定是 `this`,不是自定义名字
|
||||||
|
2. 索引器本质上还是类成员,不是数组本身
|
||||||
|
3. `[]` 不是直接重载出来的,==而是通过索引器语法提供的==
|
||||||
|
4. 如果内部是真实数组或列表,通常还要自己考虑越界问题
|
||||||
|
|
||||||
#封装
|
#封装
|
||||||
#核心
|
#核心
|
||||||
|
|||||||
+168
-25
@@ -11,28 +11,28 @@
|
|||||||
3. 让子类在父类基础上增加自己的成员和功能
|
3. 让子类在父类基础上增加自己的成员和功能
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
```Csharp
|
```csharp
|
||||||
class 子类名 : 父类名
|
class 子类名 : 父类名
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
```Csharp
|
```csharp
|
||||||
class Animal
|
class Animal
|
||||||
{
|
{
|
||||||
public string name;
|
public string name;
|
||||||
protected int age;
|
protected int age;
|
||||||
|
|
||||||
public Animal(string name)
|
public Animal(string name)
|
||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Speak()
|
public void Speak()
|
||||||
{
|
{
|
||||||
cw("动物发出声音");
|
Console.WriteLine("动物发出声音");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,12 +42,12 @@ class Dog : Animal
|
|||||||
{
|
{
|
||||||
this.age = age;
|
this.age = age;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Bark()
|
public void Bark()
|
||||||
{
|
{
|
||||||
cw(name);
|
Console.WriteLine(name);
|
||||||
cw(age);
|
Console.WriteLine(age);
|
||||||
cw("汪汪");
|
Console.WriteLine("汪汪");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +59,152 @@ d.Bark();
|
|||||||
|
|
||||||
注意:
|
注意:
|
||||||
1. ==C# 的类只能直接继承一个父类==,也就是单继承
|
1. ==C# 的类只能直接继承一个父类==,也就是单继承
|
||||||
2. 继承是可以传递的,A 继承 B,B 继承 C,那么 A 也会继承到 C 的可用成员
|
2. 继承可以传递,A 继承 B,B 继承 C,那么 A 也会拥有 C 中可继承的成员
|
||||||
3. ==构造函数不会被继承==,子类只是可以调用父类构造函数
|
3. ==构造函数不会被继承==,子类只是可以调用父类构造函数
|
||||||
4. 父类的 `private` 成员子类==不能直接访问==,想给子类用一般写 `protected`
|
4. 父类的 `private` 成员子类==不能直接访问==,想给子类用一般写 `protected`
|
||||||
5. 结构体 `struct` ==不支持继承类==,但可以实现接口
|
5. 结构体 `struct` ==不支持继承类==,但可以实现接口
|
||||||
6. 子类对象本质上也是父类对象的一种,所以继承是后面学[[多态vob|多态]]的基础
|
6. 子类对象本质上也是父类对象的一种,所以继承是后面学[[多态vob|多态]]和[[里氏替换原则]]的基础
|
||||||
|
|
||||||
|
----
|
||||||
|
# 继承中的构造函数
|
||||||
|
概念:
|
||||||
|
继承里最容易混淆的一点就是==“子类会继承父类成员,但不会继承父类构造函数”==
|
||||||
|
当你 `new` 一个子类对象时,不只是创建“子类自己的部分”,还要先把==父类那一部分==初始化好
|
||||||
|
所以子类构造函数最终一定会调用某个父类构造函数
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 让父类负责初始化“大家共有”的数据
|
||||||
|
2. 让子类只负责补充自己的初始化逻辑
|
||||||
|
3. 保证对象创建出来时就是完整、可用的状态
|
||||||
|
|
||||||
|
核心规则:
|
||||||
|
1. ==父类构造函数不会被继承==
|
||||||
|
2. ==子类构造函数最终一定会调用某个父类构造函数==
|
||||||
|
3. 如果子类没有显式写 `: base(...)`,编译器会尝试补一个 `: base()`
|
||||||
|
4. 如果父类没有无参构造函数,子类就必须显式写 `: base(...)`
|
||||||
|
5. 构造函数体的执行顺序可以先记成:==先父类,后子类==
|
||||||
|
|
||||||
|
案例1:显式调用父类构造函数
|
||||||
|
```csharp
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Person(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Console.WriteLine("Person构造");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Student : Person
|
||||||
|
{
|
||||||
|
public int Score;
|
||||||
|
|
||||||
|
public Student(string name, int score) : base(name)
|
||||||
|
{
|
||||||
|
Score = score;
|
||||||
|
Console.WriteLine("Student构造");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Student s = new Student("小明", 100);
|
||||||
|
```
|
||||||
|
|
||||||
|
执行理解:
|
||||||
|
1. 先执行 `Person(string name)`
|
||||||
|
2. 再执行 `Student(string name, int score)` 的函数体
|
||||||
|
|
||||||
|
案例2:父类有无参构造函数时,可以省略 `base()`
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public Animal()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Animal构造");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public Dog()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Dog构造");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dog d = new Dog();
|
||||||
|
```
|
||||||
|
↑ 这里虽然没写 `: base()`,但编译器会默认帮你补上
|
||||||
|
|
||||||
|
案例3:父类没有无参构造函数时,子类必须显式写 `base(...)`
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Animal(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public Dog(string name) : base(name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
错误示意:
|
||||||
|
```csharp
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public Dog()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
↑ 这时会报错,因为编译器默认想调用 `base()`,但父类根本没有无参构造函数
|
||||||
|
|
||||||
|
案例4:子类构造函数之间也可以先用 `this(...)` 串起来
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Animal(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public int Age;
|
||||||
|
|
||||||
|
public Dog() : this("默认小狗", 0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dog(string name, int age) : base(name)
|
||||||
|
{
|
||||||
|
Age = age;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
`Dog()` 没有直接 `base(name)`,而是先调用同类里的 `Dog(string name, int age)`
|
||||||
|
然后再由那个构造函数去调用 `base(name)`
|
||||||
|
所以最终调用链还是会到父类构造函数
|
||||||
|
|
||||||
|
注意:
|
||||||
|
1. 一个构造函数初始化器里==只能写一个==,也就是写了 `this(...)` 就不能再写 `base(...)`
|
||||||
|
2. 但 `this(...)` 调到的另一个构造函数里,仍然可以写 `base(...)`
|
||||||
|
3. 不要在子类里重复初始化父类已经负责的数据,父类自己的数据最好交给父类构造函数处理
|
||||||
|
4. 初学阶段先牢牢记住:==创建子类对象时,一定先把父类部分构造好==
|
||||||
|
|
||||||
----
|
----
|
||||||
# base关键字
|
# base关键字
|
||||||
@@ -76,34 +217,35 @@ d.Bark();
|
|||||||
2. 在子类方法中访问父类同名成员或父类方法
|
2. 在子类方法中访问父类同名成员或父类方法
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
```Csharp
|
```csharp
|
||||||
class Dog : Animal
|
class Dog : Animal
|
||||||
{
|
{
|
||||||
public Dog(string name) : base(name)
|
public Dog(string name) : base(name)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
调用父类成员:
|
调用父类成员:
|
||||||
```Csharp
|
```csharp
|
||||||
base.父类成员名
|
base.父类成员名
|
||||||
```
|
```
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
```Csharp
|
```csharp
|
||||||
class Father
|
class Father
|
||||||
{
|
{
|
||||||
public string name;
|
public string name;
|
||||||
|
|
||||||
public Father(string name)
|
public Father(string name)
|
||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Say()
|
public void Say()
|
||||||
{
|
{
|
||||||
cw("我是父类");
|
Console.WriteLine("我是父类");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,13 +253,12 @@ class Son : Father
|
|||||||
{
|
{
|
||||||
public Son(string name) : base(name)
|
public Son(string name) : base(name)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Test()
|
public void Test()
|
||||||
{
|
{
|
||||||
base.Say();
|
base.Say();
|
||||||
cw(base.name);
|
Console.WriteLine(base.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,14 +271,16 @@ s.Test();
|
|||||||
1. `base` 只能在==实例构造函数、实例方法、实例属性访问器==里用
|
1. `base` 只能在==实例构造函数、实例方法、实例属性访问器==里用
|
||||||
2. ==静态方法里不能用 `base`==
|
2. ==静态方法里不能用 `base`==
|
||||||
3. 如果父类没有无参构造函数,子类通常就要显式写 `: base(...)`
|
3. 如果父类没有无参构造函数,子类通常就要显式写 `: base(...)`
|
||||||
4. `base` 是访问父类,不是创建一个新的父类对象
|
4. `base` 是访问父类成员,不是创建一个新的父类对象
|
||||||
|
|
||||||
----
|
----
|
||||||
引用:
|
引用:
|
||||||
1. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
|
1. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
|
||||||
2. Microsoft Learn: [Inheritance in C# and .NET](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/inheritance)
|
2. Microsoft Learn: [Inheritance in C# and .NET](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/inheritance)
|
||||||
3. Microsoft Learn: [base (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/base)
|
3. Microsoft Learn: [Constructors (C# Programming Guide)](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/constructors)
|
||||||
4. Microsoft Learn: [protected (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/protected)
|
4. Microsoft Learn: [Using constructors](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-constructors)
|
||||||
|
5. Microsoft Learn: [base (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/base)
|
||||||
|
6. Microsoft Learn: [protected (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/protected)
|
||||||
|
|
||||||
#继承
|
#继承
|
||||||
#核心
|
#核心
|
||||||
|
|||||||
+71
-75
@@ -1,86 +1,82 @@
|
|||||||
|
# 运算符重载
|
||||||
概念:
|
概念:
|
||||||
==让自定义类和结构体能够使用运算符==
|
- 运算符重载可以让==自定义类型==像内置类型一样使用某些运算符
|
||||||
让自定义类结构体可以进行运算
|
- 本质上是给运算符提供一套你自己定义的行为
|
||||||
通常需要注意运算符需要几个参数,例如取反只有一个参数,加有两个参数
|
|
||||||
可重载的运算符:
|
|
||||||
- 算数运算符
|
|
||||||
- 逻辑运算符==非==
|
|
||||||
- 位运算符
|
|
||||||
- 条件运算符
|
|
||||||
不可重载的运算符:
|
|
||||||
- 逻辑与(&&)逻辑或(||)
|
|
||||||
- 索引符\[]
|
|
||||||
- 墙砖运算符()
|
|
||||||
- 点.
|
|
||||||
- 三目运算符: ?
|
|
||||||
- 赋值符=
|
|
||||||
|
|
||||||
|
|
||||||
通常用法:
|
通常用法:
|
||||||
|
- 坐标、向量、复数、金额等“天然适合运算”的类型
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
使用关键词operator
|
```csharp
|
||||||
作为类的一个成员存在
|
public static 返回值类型 operator 运算符(参数列表)
|
||||||
public static 返回值类型 operator 运算符(参数列表)
|
|
||||||
|
|
||||||
案例:
|
|
||||||
```Csharp
|
|
||||||
class Point
|
|
||||||
{
|
{
|
||||||
public int x;
|
// 自定义逻辑
|
||||||
public int y;
|
|
||||||
|
|
||||||
public static Point operator +(Point p1, Point p2)
|
|
||||||
{
|
|
||||||
Point newPoint = new Point();
|
|
||||||
newPoint.x = p1.x + p2.x;
|
|
||||||
newPoint.y = p1.y + p2.y;
|
|
||||||
|
|
||||||
return newPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Point operator +(Point p1, int value)
|
|
||||||
{
|
|
||||||
Point newPoint = new Point();
|
|
||||||
newPoint.x = p1.x + value;
|
|
||||||
newPoint.y = p1.y + value;
|
|
||||||
return newPoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
Point p1 = new Point();
|
|
||||||
p1.x = 1;
|
|
||||||
p1.y = 1;
|
|
||||||
|
|
||||||
Point p2 = new Point();
|
|
||||||
p2.x = 2;
|
|
||||||
p2.y = 2;
|
|
||||||
|
|
||||||
Point p3 = new Point();
|
|
||||||
p3 = p1 + p2;
|
|
||||||
Console.WriteLine(p3.x);
|
|
||||||
|
|
||||||
p3 = null;
|
|
||||||
p3 = p1 + 10;
|
|
||||||
Console.WriteLine(p3.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
第一个运算符重载了`+`号,可以让这个类的两个实例化对象相加
|
|
||||||
第二个运算符也重载了`+`号,可以让这个类加上一个value
|
|
||||||
==如果需要value在前也需要进行一次重载!!!==
|
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
class Point
|
||||||
|
{
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
|
||||||
|
public static Point operator +(Point p1, Point p2)
|
||||||
|
{
|
||||||
|
return new Point
|
||||||
|
{
|
||||||
|
X = p1.X + p2.X,
|
||||||
|
Y = p1.Y + p2.Y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Point operator +(Point p, int value)
|
||||||
|
{
|
||||||
|
return new Point
|
||||||
|
{
|
||||||
|
X = p.X + value,
|
||||||
|
Y = p.Y + value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Point p1 = new Point { X = 1, Y = 1 };
|
||||||
|
Point p2 = new Point { X = 2, Y = 2 };
|
||||||
|
|
||||||
|
Point p3 = p1 + p2;
|
||||||
|
Point p4 = p1 + 10;
|
||||||
|
Console.WriteLine($"{p3.X}, {p3.Y}");
|
||||||
|
Console.WriteLine($"{p4.X}, {p4.Y}");
|
||||||
|
```
|
||||||
|
|
||||||
|
补充:
|
||||||
|
- 如果你希望 `10 + p1` 也成立,需要再额外写一份对应重载
|
||||||
|
- 运算符重载最重要的是==语义自然==,不要让 `+` 去做奇怪的事
|
||||||
|
|
||||||
|
常见可重载运算符:
|
||||||
|
- 一元:`+` `-` `!` `~` `++` `--` `true` `false`
|
||||||
|
- 二元:`+` `-` `*` `/` `%` `&` `|` `^` `<<` `>>`
|
||||||
|
- 比较:`==` `!=` `<` `>` `<=` `>=`
|
||||||
|
|
||||||
|
不能直接重载的常见运算符:
|
||||||
|
- `&&` `||`
|
||||||
|
- `[]`
|
||||||
|
- `()`
|
||||||
|
- `.`
|
||||||
|
- `=`
|
||||||
|
- `?:`
|
||||||
|
- `??`
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
1. 一定是公共的静态方法
|
1. ==传统写法里,运算符重载通常写成 `public static`==
|
||||||
2. 返回值写在operator前
|
2. 参数里至少要有一个参数是当前类型
|
||||||
3. 逻辑处理自定义
|
3. 不能给运算符参数加 `ref` 或 `out`
|
||||||
4. 一个符号可以多个重载
|
4. 某些运算符需要成对实现:
|
||||||
5. 不能使用ref和out
|
- `==` 和 `!=`
|
||||||
6. ==条件运算符需要成对实现==
|
- `<` 和 `>`
|
||||||
|
- `<=` 和 `>=`
|
||||||
|
- `true` 和 `false`
|
||||||
|
5. 如果重载了 `==` / `!=`,通常也应该一起重写 `Equals` 和 `GetHashCode`
|
||||||
|
6. `&&` / `||` 不能直接重载,但可以结合 `true` / `false` 和 `&` / `|` 实现相关行为
|
||||||
|
|
||||||
|
#核心
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# 里氏替换原则
|
||||||
|
概念:
|
||||||
|
- 里氏替换原则(Liskov Substitution Principle,LSP)说的是:
|
||||||
|
==所有使用父类的地方,都应该能够无障碍地替换成子类,而程序行为仍然正确==
|
||||||
|
- 简单理解:
|
||||||
|
==子类必须真的能当父类来用,而不是“语法上能继承,逻辑上却不成立”==
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
1. 判断一个继承关系到底合不合理
|
||||||
|
2. 约束子类重写父类成员时,不要破坏原有规则
|
||||||
|
3. 提醒自己:如果继承后总要“特殊处理”,那通常应该改成组合或接口
|
||||||
|
|
||||||
|
核心理解:
|
||||||
|
1. 子类可以扩展父类功能
|
||||||
|
2. 但==不能破坏父类原本承诺的行为==
|
||||||
|
3. 如果父类能做的事,子类却突然“不支持”了,那往往就违反了里氏替换原则
|
||||||
|
|
||||||
|
----
|
||||||
|
# 经典反例
|
||||||
|
错误设计:
|
||||||
|
```csharp
|
||||||
|
class Bird
|
||||||
|
{
|
||||||
|
public virtual void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("鸟会飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sparrow : Bird
|
||||||
|
{
|
||||||
|
public override void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("麻雀飞走了");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Penguin : Bird
|
||||||
|
{
|
||||||
|
public override void Fly()
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("企鹅不会飞");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LetBirdFly(Bird bird)
|
||||||
|
{
|
||||||
|
bird.Fly();
|
||||||
|
}
|
||||||
|
|
||||||
|
LetBirdFly(new Sparrow()); // 正常
|
||||||
|
LetBirdFly(new Penguin()); // 出错
|
||||||
|
```
|
||||||
|
|
||||||
|
问题:
|
||||||
|
- `LetBirdFly(Bird bird)` 这个函数相信:只要传进来的是 `Bird`,就能 `Fly()`
|
||||||
|
- 但 `Penguin` 继承了 `Bird` 后,却把“会飞”这个规则破坏了
|
||||||
|
- 这就说明:==`Penguin` 其实不应该直接继承这种“默认会飞”的 Bird==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 更合理的改法
|
||||||
|
改法一:把“鸟”和“会飞”拆开
|
||||||
|
```csharp
|
||||||
|
abstract class Bird
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFlyable
|
||||||
|
{
|
||||||
|
void Fly();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sparrow : Bird, IFlyable
|
||||||
|
{
|
||||||
|
public void Fly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("麻雀飞走了");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Penguin : Bird
|
||||||
|
{
|
||||||
|
public void Swim()
|
||||||
|
{
|
||||||
|
Console.WriteLine("企鹅在游泳");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LetBirdFly(IFlyable bird)
|
||||||
|
{
|
||||||
|
bird.Fly();
|
||||||
|
}
|
||||||
|
|
||||||
|
LetBirdFly(new Sparrow()); // 正常
|
||||||
|
// LetBirdFly(new Penguin()); // 编译期就不允许,更合理
|
||||||
|
```
|
||||||
|
|
||||||
|
这样做的好处:
|
||||||
|
1. `Bird` 只表达“它是一只鸟”
|
||||||
|
2. `IFlyable` 只表达“它会飞”
|
||||||
|
3. 不会飞的鸟就不用为了继承而硬实现一个错误行为
|
||||||
|
|
||||||
|
----
|
||||||
|
# 怎么判断有没有违反
|
||||||
|
可以拿下面几个问题自检:
|
||||||
|
|
||||||
|
1. 子类是不是父类的一种?
|
||||||
|
- 如果读起来都别扭,那继承关系大概率有问题
|
||||||
|
2. 把子类对象传到父类参数的位置,原有代码还能正常工作吗?
|
||||||
|
- 如果经常要加 `if (对象是某某子类)`,设计就开始危险了
|
||||||
|
3. 子类有没有把父类原本支持的行为变成“不支持”?
|
||||||
|
- 例如:父类能 `Fly()`,子类却直接抛异常
|
||||||
|
4. 子类有没有偷偷加强限制?
|
||||||
|
- 父类允许任意正数,子类却只允许 1 到 10
|
||||||
|
5. 子类有没有把父类承诺的结果变弱?
|
||||||
|
- 父类保证成功,子类却经常返回空结果或失败
|
||||||
|
|
||||||
|
----
|
||||||
|
# 违反里氏替换原则的常见信号
|
||||||
|
1. 子类重写后直接抛 `NotSupportedException`
|
||||||
|
2. 子类必须靠很多 `if/else` 特判才能安全使用
|
||||||
|
3. 继承只是为了复用代码,但语义上并不是“is-a”关系
|
||||||
|
4. 子类把父类的规则改掉了,而不是扩展
|
||||||
|
|
||||||
|
补充:
|
||||||
|
- “能继承”不等于“该继承”
|
||||||
|
- 很多违反 LSP 的代码,表面上编译没问题,真正出问题的是业务逻辑
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和继承的关系
|
||||||
|
里氏替换原则不是反对继承,而是告诉你:
|
||||||
|
|
||||||
|
- ==继承必须建立在真正的替换关系上==
|
||||||
|
- 如果子类不能稳定替换父类,就不要硬继承
|
||||||
|
- 这时通常优先考虑:
|
||||||
|
- 组合
|
||||||
|
- 接口
|
||||||
|
- 更细的抽象拆分
|
||||||
|
|
||||||
|
所以可以把它和[[继承]]一起记:
|
||||||
|
- 继承解决“复用和扩展”
|
||||||
|
- 里氏替换原则约束“这个继承是不是合理”
|
||||||
|
|
||||||
|
----
|
||||||
|
# `is` 和 `as` 关键词怎么结合理解
|
||||||
|
先说结论:
|
||||||
|
|
||||||
|
- `is` 和 `as` 是==类型判断 / 类型转换工具==
|
||||||
|
- 它们能帮你在运行时判断“这个对象现在到底是什么类型”
|
||||||
|
- 但它们==不能证明一个继承设计就合理==
|
||||||
|
|
||||||
|
## `is`
|
||||||
|
作用:
|
||||||
|
- 判断对象是否兼容某个类型
|
||||||
|
- 也常用于模式匹配
|
||||||
|
|
||||||
|
例子:
|
||||||
|
```csharp
|
||||||
|
Bird bird = new Sparrow();
|
||||||
|
|
||||||
|
if (bird is Sparrow)
|
||||||
|
{
|
||||||
|
Console.WriteLine("这是麻雀");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bird is IFlyable flyable)
|
||||||
|
{
|
||||||
|
flyable.Fly();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
- `bird is Sparrow` 的意思是:当前这个对象是不是 `Sparrow`
|
||||||
|
- `bird is IFlyable flyable` 不仅判断类型,还顺手把结果保存到变量 `flyable`
|
||||||
|
|
||||||
|
## `as`
|
||||||
|
作用:
|
||||||
|
- 尝试把对象安全转换成某个引用类型或可空值类型
|
||||||
|
- 转换失败不会抛异常,而是返回 `null`
|
||||||
|
|
||||||
|
例子:
|
||||||
|
```csharp
|
||||||
|
Bird bird = new Sparrow();
|
||||||
|
|
||||||
|
IFlyable flyable = bird as IFlyable;
|
||||||
|
if (flyable != null)
|
||||||
|
{
|
||||||
|
flyable.Fly();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
补充:
|
||||||
|
- `as` 常用于引用类型
|
||||||
|
- 如果转换失败,结果是 `null`
|
||||||
|
- 对普通非可空值类型不能直接用 `as`
|
||||||
|
|
||||||
|
## 它和里氏替换原则的关系
|
||||||
|
`is` / `as` 本身没有错,但如果你发现代码里总是这样:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
void Handle(Bird bird)
|
||||||
|
{
|
||||||
|
if (bird is Sparrow)
|
||||||
|
{
|
||||||
|
// 一套逻辑
|
||||||
|
}
|
||||||
|
else if (bird is Penguin)
|
||||||
|
{
|
||||||
|
// 另一套逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
那通常说明一件事:
|
||||||
|
- 你嘴上写的是“我接收 `Bird`”
|
||||||
|
- 但你心里真正依赖的却是“具体子类是谁”
|
||||||
|
|
||||||
|
这往往意味着:
|
||||||
|
1. 抽象不够稳定
|
||||||
|
2. 多态没有真正用起来
|
||||||
|
3. 继承关系可能不够合理
|
||||||
|
4. 有概率正在逼着代码违反里氏替换原则
|
||||||
|
|
||||||
|
更好的方向一般是:
|
||||||
|
- 让父类 / 接口直接提供调用方真正需要的能力
|
||||||
|
- 或者把能力拆到更合适的接口里
|
||||||
|
|
||||||
|
一句话记:
|
||||||
|
- `is` / `as` 是“识别类型”的工具
|
||||||
|
- 里氏替换原则是“约束继承是否合理”的原则
|
||||||
|
- ==如果父类代码里到处都要靠 `is` / `as` 分辨子类,通常该反过来检查抽象设计==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==子类可以扩展父类,但不能打破父类原来的承诺==
|
||||||
|
|
||||||
|
再压缩一点:
|
||||||
|
==父类能出现的地方,子类也必须能正常出现==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“里氏替换原则的意思是,子类对象必须能够替换父类对象,并且不影响程序原本的正确性。
|
||||||
|
如果一个子类继承了父类,却把父类原本支持的行为改成异常、限制更多条件,或者导致调用方需要额外特殊判断,那这个继承关系通常就不合理。
|
||||||
|
这时候应该考虑重新抽象,或者改成接口和组合。”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. 里氏替换原则是==设计原则==,不是语法规则
|
||||||
|
2. 它常和[[继承]]、[[多态vob|多态]]、[[接口]]一起理解
|
||||||
|
3. `is` / `as` 可以辅助做类型判断,但不能代替抽象设计
|
||||||
|
4. 只要你发现“某个子类总是不太像父类”,就该警惕是不是违反了 LSP
|
||||||
|
|
||||||
|
#核心
|
||||||
|
#设计原则
|
||||||
+49
-32
@@ -1,51 +1,68 @@
|
|||||||
|
# 静态成员
|
||||||
概念:
|
概念:
|
||||||
用static 修饰的成员变量、方法、属性
|
- 用 `static` 修饰的成员叫静态成员
|
||||||
|
- 静态成员属于==类型本身==,不属于某个具体对象
|
||||||
|
- 常见的静态成员有:静态字段、静态方法、静态属性、静态事件
|
||||||
|
|
||||||
特点:
|
特点:
|
||||||
==直接用类名点出来使用 没有实例化!!!!==
|
- 一般通过==类名.成员名==访问
|
||||||
|
- 同一类型的所有对象共享同一份静态数据
|
||||||
|
|
||||||
通常用法:
|
通常用法:
|
||||||
PI之类的全局且唯一性的
|
- 记录全局共享状态
|
||||||
|
- 写工具方法
|
||||||
语法:
|
- 定义不会因对象不同而变化的数据
|
||||||
static 变量名 = XXX;
|
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
```Csharp
|
```csharp
|
||||||
class Test
|
class Player
|
||||||
{
|
{
|
||||||
static public float PI = 3.1415926; //静态成员
|
public static int Count;
|
||||||
public int testInt = 100;
|
public string Name;
|
||||||
|
|
||||||
public static float CalcCircle(float r) //静态方法
|
public Player(string name)
|
||||||
{
|
{
|
||||||
return PI * r;
|
Name = name;
|
||||||
|
Count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TestFunc() //非静态方法
|
public static void PrintCount()
|
||||||
{
|
{
|
||||||
Console.WriteLine("123");
|
Console.WriteLine(Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Main(string[] args)
|
Player p1 = new Player("A");
|
||||||
{
|
Player p2 = new Player("B");
|
||||||
Console.WriteLine(Test.PI);
|
Player.PrintCount(); // 2
|
||||||
Console.WriteLine(Test.CalcCircle(2));
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
和程序同生共死,在程序使用后永远不会清除内存,直到程序结束
|
1. ==静态成员属于类,不属于对象==
|
||||||
非静态函数可以调用静态成员
|
2. 静态方法里不能直接访问实例成员,因为它没有 `this`
|
||||||
静态成员声明后可以不赋初始值
|
3. 实例方法里可以访问静态成员
|
||||||
静态方法中不能使用==没有实例化的==非静态成员
|
4. 静态字段通常会随着类型存在而存在;在大多数日常程序里,可以近似理解为“程序结束前一直都在”
|
||||||
|
5. 静态成员不是“永远不释放”的绝对规则,别把它和 GC 机制混为一谈
|
||||||
|
|
||||||
|
[[常量|const]] 和 `static readonly`:
|
||||||
|
共同点:
|
||||||
|
- 都常用来表示“不希望随便改”的值
|
||||||
|
|
||||||
[[常量]]可以理解为特殊的静态
|
区别:
|
||||||
共同点
|
1. `const`
|
||||||
- 都可以通过类名点出来使用
|
- 必须声明时初始化
|
||||||
不同点
|
- 编译期常量
|
||||||
- const必须初始化,不能修改 static 没有这个限制
|
- 隐式就是静态语义
|
||||||
- const值能修饰变量、static可以修饰很多
|
2. `static readonly`
|
||||||
- const一定是写在访问修饰符后面的,static没有这个要求
|
- 可以声明时赋值,也可以在静态构造函数中赋值
|
||||||
|
- 运行期确定
|
||||||
|
- 更适合保存复杂初始化结果
|
||||||
|
|
||||||
|
例子:
|
||||||
|
```csharp
|
||||||
|
class MathInfo
|
||||||
|
{
|
||||||
|
public const float Pi = 3.1415926f;
|
||||||
|
public static readonly DateTime StartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
+60
-50
@@ -1,70 +1,80 @@
|
|||||||
# 静态类
|
# 静态类
|
||||||
概念:
|
概念:
|
||||||
用static修饰
|
- 用 `static` 修饰的类叫静态类
|
||||||
==只能包含静态成员==
|
- ==只能包含静态成员==
|
||||||
==不能被实例化==
|
- ==不能被实例化==
|
||||||
|
|
||||||
通常用法:
|
通常用法:
|
||||||
作为==工具类==方便使用
|
- 写工具类
|
||||||
|
- 存放通用辅助方法
|
||||||
|
|
||||||
语法:
|
语法:
|
||||||
访问修饰符 static 类名(){}
|
```csharp
|
||||||
|
public static class MathTool
|
||||||
案例:
|
|
||||||
```Csharp
|
|
||||||
public static class fun()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:
|
|
||||||
静态类不能被实例化,所以不能new!
|
|
||||||
|
|
||||||
# 静态构造函数
|
|
||||||
概念:
|
|
||||||
staitc修饰
|
|
||||||
|
|
||||||
通常用法:
|
|
||||||
在静态函数中初始化静态变量
|
|
||||||
|
|
||||||
语法:
|
|
||||||
|
|
||||||
|
|
||||||
案例:
|
|
||||||
```Csharp
|
|
||||||
static class StaticClass
|
|
||||||
{
|
{
|
||||||
public static int testInt = 100;
|
public static int Add(int a, int b)
|
||||||
public static int testInt2 = 100;
|
|
||||||
|
|
||||||
static StaticClass()
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("静态构造函数");
|
return a + b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
↑运行后,在第一次使用该类会直接打印
|
|
||||||
```Csharp
|
注意:
|
||||||
|
1. 静态类不能 `new`
|
||||||
|
2. 静态类不能包含实例成员
|
||||||
|
3. 静态类本质上也不能被继承,常可以理解为“天然密封”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 静态构造函数
|
||||||
|
概念:
|
||||||
|
- 静态构造函数用于初始化静态数据
|
||||||
|
- 会在==第一次使用该类型前==自动执行
|
||||||
|
|
||||||
|
通常用法:
|
||||||
|
- 初始化复杂的静态字段
|
||||||
|
- 做一次性的类型级初始化
|
||||||
|
|
||||||
|
语法:
|
||||||
|
```csharp
|
||||||
class Test
|
class Test
|
||||||
{
|
{
|
||||||
public static int testInt = 200;
|
|
||||||
|
|
||||||
static Test()
|
static Test()
|
||||||
{
|
{
|
||||||
Console.WriteLine("静态构造函数");
|
Console.WriteLine("静态构造函数");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Test()
|
|
||||||
{
|
|
||||||
Console.WriteLine("普通构造函数");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
↑运行后,在第一次使用该类会在所有操作前先调用静态构造函数
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
class Test
|
||||||
|
{
|
||||||
|
public static int Count;
|
||||||
|
|
||||||
|
static Test()
|
||||||
|
{
|
||||||
|
Count = 100;
|
||||||
|
Console.WriteLine("执行静态构造函数");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Test()
|
||||||
|
{
|
||||||
|
Console.WriteLine("执行普通构造函数");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(Test.Count);
|
||||||
|
var t = new Test();
|
||||||
|
```
|
||||||
|
|
||||||
|
执行理解:
|
||||||
|
- 第一次访问 `Test.Count` 时,会先执行静态构造函数
|
||||||
|
- 之后再创建对象时,不会重复执行静态构造函数
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
1. 静态类和普通类都可以用
|
1. ==类和结构体都可以有静态构造函数==
|
||||||
2. 不能使用访问修饰符
|
2. ==静态构造函数不能写访问修饰符==
|
||||||
3. ==不能有参数==
|
3. ==静态构造函数不能有参数==
|
||||||
4. 只会==自动调用一次==
|
4. 一个类型最多只有一个静态构造函数
|
||||||
|
5. 静态构造函数只会自动执行一次
|
||||||
|
6. 如果只是简单赋值,优先考虑直接写静态字段初始化器,通常更直观
|
||||||
|
|||||||
@@ -1,793 +0,0 @@
|
|||||||
# CSharp 复习题库(分层练习)
|
|
||||||
|
|
||||||
> 使用方式:每题都在“你的作答”填写答案;“批改区”由我批改后填写。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 入门(16题)
|
|
||||||
|
|
||||||
### 入门-1 [选择]
|
|
||||||
C# 源文件常见扩展名是:A. `.cs` B. `.csharp` C. `.dotnet` D. `.clr`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
A
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:A
|
|
||||||
|
|
||||||
### 入门-2 [选择]
|
|
||||||
`Main` 方法作为入口,正确签名之一是:A. `void main()` B. `static void Main(string[] args)` C. `public Main()` D. `int Main(void)`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
B
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:B
|
|
||||||
|
|
||||||
### 入门-3 [选择]
|
|
||||||
下列哪个是值类型:A. `string` B. `object` C. `int` D. `Array`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
C
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:C
|
|
||||||
|
|
||||||
### 入门-4 [选择]
|
|
||||||
`var x = 10;` 中 `x` 的编译期类型是:A. `dynamic` B. `object` C. `int` D. `long`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
C
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:C
|
|
||||||
|
|
||||||
### 入门-5 [选择]
|
|
||||||
输出到控制台应使用:A. `Console.ReadLine` B. `Console.WriteLine` C. `Console.Open` D. `Console.Print`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
B
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:B
|
|
||||||
|
|
||||||
### 入门-6 [填空]
|
|
||||||
用于单行注释的符号是 `____`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
`//`
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:`//`
|
|
||||||
|
|
||||||
### 入门-7 [填空]
|
|
||||||
`if` 条件表达式的结果类型必须是 `____`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
`bool`
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:`bool`
|
|
||||||
|
|
||||||
### 入门-8 [填空]
|
|
||||||
将字符串 `"123"` 转 `int`,可用 `int.Parse("123")` 或 `____("123", out n)`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
`Convert.Int32("123", out n)`
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:❌ 错误
|
|
||||||
- 参考答案:`int.TryParse("123", out n)`
|
|
||||||
- 说明:`Convert.ToInt32` 没有 `out` 参数;带 `out` 的是 `TryParse`。
|
|
||||||
|
|
||||||
### 入门-9 [判断]
|
|
||||||
`const` 字段的值可以在运行时修改。对/错
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
错
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:错
|
|
||||||
|
|
||||||
### 入门-10 [判断]
|
|
||||||
`string` 在 C# 中是引用类型。对/错
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
对
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:对
|
|
||||||
|
|
||||||
### 入门-11 [代码结果]
|
|
||||||
写出输出:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
int a = 5;
|
|
||||||
int b = 2;
|
|
||||||
Console.WriteLine(a / b);
|
|
||||||
Console.WriteLine(a / 2.0);
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
`2`
|
|
||||||
`1.0`
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:❌ 错误
|
|
||||||
- 参考答案:
|
|
||||||
```text
|
|
||||||
2
|
|
||||||
2.5
|
|
||||||
```
|
|
||||||
- 说明:`a / b` 是整数除法,结果为 `2`。
|
|
||||||
|
|
||||||
### 入门-12 [代码改错]
|
|
||||||
修复下面代码使其可编译:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
Console.Writeline("Hello")
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
```csharp
|
|
||||||
Console.Writeline("Hello");
|
|
||||||
```
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:❌ 错误
|
|
||||||
- 参考答案:
|
|
||||||
```csharp
|
|
||||||
Console.WriteLine("Hello");
|
|
||||||
```
|
|
||||||
- 说明:`WriteLine` 中 `L` 需要大写。
|
|
||||||
|
|
||||||
### 入门-13 [简答]
|
|
||||||
`==` 与 `Equals` 的常见区别是什么(1-2句话)
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
`Equals` 可以用于更复杂判断,`==` 只比较值。
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:⚠️ 部分正确
|
|
||||||
- 参考要点:
|
|
||||||
- `==` 是运算符,可被重载;不同类型语义可能不同。
|
|
||||||
- `Equals` 是方法(实例/静态),常用于值相等判断。
|
|
||||||
- 对 `string` 来说,`==` 比较的是内容(被重载),不是引用地址。
|
|
||||||
|
|
||||||
### 入门-14 [编程]
|
|
||||||
从控制台读取姓名和年龄,输出:`你好,张三,你明年 19 岁。`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
```csharp
|
|
||||||
string name = Console.Readline();
|
|
||||||
int age = int.Parse(Console.Readline());
|
|
||||||
Console.WriteLine($"你好,{name},你明年 {age} 岁。");
|
|
||||||
```
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:❌ 错误
|
|
||||||
- 参考答案:
|
|
||||||
```csharp
|
|
||||||
string name = Console.ReadLine();
|
|
||||||
int age = int.Parse(Console.ReadLine()!);
|
|
||||||
Console.WriteLine($"你好,{name},你明年 {age + 1} 岁。");
|
|
||||||
```
|
|
||||||
- 说明:`ReadLine` 大小写错误,且“明年”应输出 `age + 1`。
|
|
||||||
|
|
||||||
### 入门-15 [编程]
|
|
||||||
输入一个整数,判断奇偶并输出 `odd/even`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
```Csharp
|
|
||||||
int value = int.Parse(Console.ReadLine());
|
|
||||||
if( value%2 != 0 )
|
|
||||||
{
|
|
||||||
Console.WriteLine("odd");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("even");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:❌ 错误
|
|
||||||
- 主要问题:`int.TryParse(...)` 返回 `bool`,不能直接赋给 `int`
|
|
||||||
- 参考答案:
|
|
||||||
```csharp
|
|
||||||
if (int.TryParse(Console.ReadLine(), out int value))
|
|
||||||
Console.WriteLine(value % 2 == 0 ? "even" : "odd");
|
|
||||||
```
|
|
||||||
|
|
||||||
### 入门-16 [编程]
|
|
||||||
输入三个整数,输出最大值
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
```Csharp
|
|
||||||
int[] value;
|
|
||||||
int maxValue = 0;
|
|
||||||
value[0] = int.Parse(Console.ReadLine());
|
|
||||||
value[1] = int.Parse(Console.ReadLine());
|
|
||||||
value[2] = int.Parse(Console.ReadLine());
|
|
||||||
for(int i = 0;i<2;i++)
|
|
||||||
{
|
|
||||||
if(value[i]>=value[i+1])
|
|
||||||
{
|
|
||||||
maxValue = value[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Console.WriteLine(maxValue);
|
|
||||||
```
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:❌ 错误
|
|
||||||
- 主要问题:数组未初始化
|
|
||||||
- 参考答案:
|
|
||||||
```csharp
|
|
||||||
int a = int.Parse(Console.ReadLine()!);
|
|
||||||
int b = int.Parse(Console.ReadLine()!);
|
|
||||||
int c = int.Parse(Console.ReadLine()!);
|
|
||||||
int maxValue = Math.Max(a, Math.Max(b, c));
|
|
||||||
Console.WriteLine(maxValue);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
## 基础(18题)
|
|
||||||
|
|
||||||
### 基础-1 [选择]
|
|
||||||
`for` 循环最适合:A. 已知次数迭代 B. 无限循环 C. 异常处理 D. 命名空间管理
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
A
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:A
|
|
||||||
|
|
||||||
### 基础-2 [选择]
|
|
||||||
`break` 在循环中的作用是:A. 跳过本次 B. 结束当前循环 C. 结束程序 D. 重启循环
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
B
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:B
|
|
||||||
|
|
||||||
### 基础-3 [选择]
|
|
||||||
`continue` 的作用是:A. 结束方法 B. 退出程序 C. 跳过本次迭代 D. 抛出异常
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
C
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:C
|
|
||||||
|
|
||||||
### 基础-4 [选择]
|
|
||||||
数组声明正确的是:A. `int arr = new int[3]` B. `int[] arr = new int[3]` C. `array<int> arr` D. `int arr[] = 3`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
B
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:B
|
|
||||||
|
|
||||||
### 基础-5 [选择]
|
|
||||||
`List<T>` 位于命名空间:A. `System.Text` B. `System.Collections.Generic` C. `System.Linq.Expressions` D. `System.IO`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
B
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:B
|
|
||||||
|
|
||||||
### 基础-6 [填空]
|
|
||||||
定义一个可空整型变量:`int____ x = null;`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
?
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:`?`
|
|
||||||
|
|
||||||
### 基础-7 [填空]
|
|
||||||
声明并初始化字典:`Dictionary<string,int> d = new ____();`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
`Dictionary<string,int>
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:✅ 正确
|
|
||||||
- 参考答案:`Dictionary<string,int>`
|
|
||||||
- 说明:你填的类型名是对的,只是 Markdown 里的反引号没闭合。
|
|
||||||
|
|
||||||
### 基础-8 [填空]
|
|
||||||
捕获异常的关键字是 `____`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
try和catch
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
- 结果:⚠️ 部分正确
|
|
||||||
- 参考答案:`catch`
|
|
||||||
- 说明:`try` 用来包裹可能抛异常的代码,真正负责捕获异常的是 `catch`。
|
|
||||||
|
|
||||||
### 基础-9 [填空]
|
|
||||||
释放非托管资源常用模式:实现 `____` 接口
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-10 [代码结果]
|
|
||||||
写出输出:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
for (int i = 0; i < 5; i++)
|
|
||||||
{
|
|
||||||
if (i == 2) continue;
|
|
||||||
if (i == 4) break;
|
|
||||||
Console.Write(i);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-11 [代码结果]
|
|
||||||
写出输出:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
string s = null;
|
|
||||||
Console.WriteLine(s?.Length ?? -1);
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-12 [改错]
|
|
||||||
找出并修复问题:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
int[] nums = {1,2,3};
|
|
||||||
Console.WriteLine(nums[3]);
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-13 [简答]
|
|
||||||
`throw;` 和 `throw ex;` 的差异是什么
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-14 [编程]
|
|
||||||
用 `while` 计算 `1..n` 的和(n 从输入读取)
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-15 [编程]
|
|
||||||
统计一个字符串中元音字母个数(忽略大小写)
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-16 [编程]
|
|
||||||
手写一个方法 `bool IsPrime(int n)`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-17 [编程]
|
|
||||||
使用 `List<int>` 去重并升序输出
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 基础-18 [编程]
|
|
||||||
使用 `Dictionary<string,int>` 统计一句话中每个单词出现次数
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 核心(18题)
|
|
||||||
|
|
||||||
### 核心-1 [选择]
|
|
||||||
面向对象三大特性不包括:A. 封装 B. 继承 C. 多态 D. 事务
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-2 [选择]
|
|
||||||
`virtual` + `override` 用于:A. 字段隐藏 B. 运行时多态 C. 命名空间引用 D. 泛型约束
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-3 [选择]
|
|
||||||
`abstract` 类:A. 可直接实例化 B. 不能包含实现 C. 不能被继承 D. 不能直接实例化
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-4 [选择]
|
|
||||||
`interface` 成员默认:A. private B. protected C. public 合同 D. internal
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-5 [选择]
|
|
||||||
泛型约束 `where T : class` 的含义是:A. 值类型 B. 引用类型 C. 必须有无参构造 D. 必须实现 IDisposable
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-6 [填空]
|
|
||||||
声明一个自动属性:`public int Age { ____; ____; }`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-7 [填空]
|
|
||||||
只读自动属性常见写法:`public string Id { get; ____; }`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-8 [填空]
|
|
||||||
委托声明关键字是 `____`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-9 [填空]
|
|
||||||
事件订阅语法:`publisher.SomeEvent ____ Handler;`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-10 [代码阅读]
|
|
||||||
输出是什么:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
class A { public virtual void M() => Console.Write("A"); }
|
|
||||||
class B : A { public override void M() => Console.Write("B"); }
|
|
||||||
A x = new B();
|
|
||||||
x.M();
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-11 [代码阅读]
|
|
||||||
输出是什么:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
int Add(int a, int b = 10) => a + b;
|
|
||||||
Console.WriteLine(Add(5));
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-12 [改错]
|
|
||||||
指出并修复:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
interface IRun { void Go() { Console.WriteLine("go"); } }
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-13 [简答]
|
|
||||||
`override` 与 `new` 隐藏成员的行为区别
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-14 [简答]
|
|
||||||
什么时候用接口,什么时候用抽象类(简述判断标准)
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-15 [编程]
|
|
||||||
设计 `Shape` 抽象类,派生 `Circle/Rectangle`,实现 `GetArea()`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-16 [编程]
|
|
||||||
实现泛型方法 `Swap<T>(ref T a, ref T b)`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-17 [编程]
|
|
||||||
定义一个 `event EventHandler<DataEventArgs>`,在数据变化时触发
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 核心-18 [编程]
|
|
||||||
实现一个只读集合包装(对外不可修改,内部可维护)
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 进阶(18题)
|
|
||||||
|
|
||||||
### 进阶-1 [选择]
|
|
||||||
`async/await` 中,`await` 作用是:A. 创建线程 B. 异步等待任务完成 C. 阻塞 UI 线程 D. 强制并行
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-2 [选择]
|
|
||||||
表示“一个无返回值异步操作”的常用返回类型:A. `void` B. `Task` C. `object` D. `Result`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-3 [选择]
|
|
||||||
LINQ 中用于投影的操作符:A. `Where` B. `Select` C. `Any` D. `Count`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-4 [选择]
|
|
||||||
延迟执行是 LINQ 的常见特性之一,以下通常会立即执行的是:A. `Where` B. `Select` C. `ToList` D. `OrderBy`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-5 [选择]
|
|
||||||
`using` 语句主要用于:A. 导入命名空间 B. 自动释放资源 C. 定义别名 D. 启动任务
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-6 [填空]
|
|
||||||
声明取消令牌源:`var cts = new ____();`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-7 [填空]
|
|
||||||
正则匹配常用类型:`System.Text.RegularExpressions.____`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-8 [填空]
|
|
||||||
线程安全递增可用:`Interlocked.____(ref x);`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-9 [填空]
|
|
||||||
文件全部文本读取:`File.____(path)`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-10 [代码结果]
|
|
||||||
写出输出:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var nums = new[] {1,2,3,4,5};
|
|
||||||
var q = nums.Where(x => x % 2 == 1).Select(x => x * x);
|
|
||||||
Console.WriteLine(string.Join(",", q));
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-11 [代码结果]
|
|
||||||
判断是否死锁风险并说明一句原因:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
Task.Run(async () => { await Task.Delay(100); }).Wait();
|
|
||||||
```
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-12 [简答]
|
|
||||||
`Task.WhenAll` 与顺序 `await` 的区别
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-13 [简答]
|
|
||||||
`IEnumerable<T>` 与 `IQueryable<T>` 的核心差异
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-14 [编程]
|
|
||||||
异步读取文本文件并统计行数,支持取消(`CancellationToken`)
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-15 [编程]
|
|
||||||
用 LINQ 完成:给定学生列表,按班级分组后求每组平均分并降序输出
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-16 [编程]
|
|
||||||
实现一个简单缓存 `MemoryCache<TKey,TValue>`(带过期时间)
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-17 [编程]
|
|
||||||
写一个扩展方法 `IsNullOrEmptyEx(this string? s)`
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
|
|
||||||
### 进阶-18 [编程]
|
|
||||||
设计一个可并发调用的计数器类,要求线程安全
|
|
||||||
|
|
||||||
**你的作答**
|
|
||||||
(待填写)
|
|
||||||
|
|
||||||
**批改区**
|
|
||||||
(待批改)
|
|
||||||
@@ -0,0 +1,871 @@
|
|||||||
|
# CSharp 综合练习 01
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
- 本题单依据你当前已有笔记整理,范围主要来自:
|
||||||
|
- `基本概念`
|
||||||
|
- `入门:常量、转义字符、格式转换、字符串拼接、三目运算符`
|
||||||
|
- `基础:值类型和引用类型、ref 和 out、变长参数、枚举`
|
||||||
|
- `核心:继承、静态成员、成员属性、索引器`
|
||||||
|
- 你可以直接在每道题下方的 `你的作答` 区域填写答案。
|
||||||
|
- 我后续会在 `批改区` 直接给你批改和点评。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 入门
|
||||||
|
|
||||||
|
### 1. [填空题] 面向对象基础概念
|
||||||
|
根据你的笔记,完成下面填空:
|
||||||
|
|
||||||
|
1. 面向对象三大特性分别是:`____`、`____`、`____`
|
||||||
|
2. “对扩展开放,对修改关闭”指的是 `____原则`
|
||||||
|
3. “优先使用组合,而不是为了复用就滥用继承”指的是 `____原则`
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.继承、封装、多态
|
||||||
|
2.开闭原则
|
||||||
|
3.合成复用原则
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:三大特性、开闭原则、合成复用原则都答对了。第 1 小题的顺序不影响正确性,更常见的写法是“封装、继承、多态”。
|
||||||
|
|
||||||
|
### 2. [简答题] const
|
||||||
|
根据 `常量.md`,回答下面问题:
|
||||||
|
|
||||||
|
1. `const` 声明的变量有哪些核心特点?
|
||||||
|
2. 请写出两个适合使用 `const` 的场景。
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.不能修改、必须初始化
|
||||||
|
2.定义PI、重力加速度等需要某个常量的时候
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:你抓住了 `const` 的两个核心点:必须初始化、之后不能修改;场景举例也合适,比如 `PI`、重力加速度这类固定值。
|
||||||
|
|
||||||
|
### 3. [代码阅读题] 转义字符
|
||||||
|
阅读下面代码,写出三次输出的大致结果,并说明分别用到了哪些转义字符。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
string title = "\"C#入门\"";
|
||||||
|
string path = "C:\\Users\\Kister";
|
||||||
|
string text = "第一行\n第二行";
|
||||||
|
|
||||||
|
Console.WriteLine(title);
|
||||||
|
Console.WriteLine(path);
|
||||||
|
Console.WriteLine(text);
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
"C#入门"
|
||||||
|
C:\Users\Kister
|
||||||
|
第一行
|
||||||
|
第二行
|
||||||
|
|
||||||
|
\"把"保留在字符串里了
|
||||||
|
\\把\保留在字符串里了
|
||||||
|
\n是换行
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:输出结果和三个转义字符的作用都写对了:`\"` 保留双引号,`\\` 保留反斜杠,`\n` 表示换行。
|
||||||
|
|
||||||
|
### 4. [代码阅读题] 格式转换
|
||||||
|
阅读下面代码,并回答问题:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
int a = 10;
|
||||||
|
double b = a;
|
||||||
|
int c = (int)12.8;
|
||||||
|
string s = 123.ToString();
|
||||||
|
```
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
1. 哪一行发生了隐式转换?
|
||||||
|
2. 哪一行发生了显式转换?
|
||||||
|
3. 哪一行把其他类型转成了 `string`?
|
||||||
|
4. 为什么 `Parse` 方法通常更适合“字符串转数字”这类场景?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.第2行
|
||||||
|
2.第3、4行
|
||||||
|
3.第四行
|
||||||
|
4.因为转不了数字对象会报错
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 第 1 问答对了,隐式转换是第 2 行:`double b = a;`
|
||||||
|
- 第 2 问应主要写第 3 行:`int c = (int)12.8;`
|
||||||
|
- 第 4 行 `123.ToString()` 更适合归类为“转成字符串”,不算这里重点考的强制类型转换
|
||||||
|
- 第 4 问你的回答有点偏了。更准确地说:`Parse` 本来就是专门把字符串内容解析成目标数字类型的方法,所以特别常用于“字符串转数字”
|
||||||
|
|
||||||
|
### 5. [代码阅读题] 字符串拼接和三目运算符
|
||||||
|
写出下面变量的最终值,并说明原因。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
string str1 = "" + 1 + 2 + 3;
|
||||||
|
string str2 = 1 + 2 + 3 + "";
|
||||||
|
string result = 5 > 3 ? "真" : "假";
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
str1为"123",""让string知道这是在字符串拼接,123会作为字符串拼起来
|
||||||
|
str2为"6",""在最后,所以前面做的是运算而非拼接
|
||||||
|
result为"真"
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:`str1` 是 `"123"`,`str2` 是 `"6"`,`result` 是 `"真"`,你的原因分析也对,说明你已经能区分“先做数值运算”还是“先做字符串拼接”了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 基础
|
||||||
|
|
||||||
|
### 6. [判断题] 值类型和引用类型
|
||||||
|
请判断下面说法是对还是错,如果错了请改正:
|
||||||
|
|
||||||
|
1. `int` 是引用类型
|
||||||
|
2. `string` 是引用类型
|
||||||
|
3. 数组是值类型
|
||||||
|
4. 值类型赋值时通常复制的是数据本身
|
||||||
|
5. 引用类型变量保存的是对象的引用
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.错,int是典型值类型
|
||||||
|
2.对
|
||||||
|
3.错,数组存放的是值,本身是引用类型
|
||||||
|
4.对
|
||||||
|
5.对
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:基本正确。
|
||||||
|
|
||||||
|
点评:整体判断对了。第 3 小题建议表述成“数组本身是引用类型,数组元素可以是值类型也可以是引用类型”,这样会更严谨,不要把“数组存放的是值”说得太绝对。
|
||||||
|
|
||||||
|
### 7. [代码阅读题] 传值和传引用
|
||||||
|
阅读下面代码,写出最终输出结果,并解释原因。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static void ChangeA(int x)
|
||||||
|
{
|
||||||
|
x = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ChangeB(int[] arr)
|
||||||
|
{
|
||||||
|
arr[0] = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
int a = 1;
|
||||||
|
int[] b = { 1 };
|
||||||
|
|
||||||
|
ChangeA(a);
|
||||||
|
ChangeB(b);
|
||||||
|
|
||||||
|
Console.WriteLine(a);
|
||||||
|
Console.WriteLine(b[0]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1
|
||||||
|
100
|
||||||
|
a作为值类型传递值本身而不是地址,函数内部更改的是函数内部x地址的值,所以外部还是1
|
||||||
|
数组b作为引用类型传递的是地址,函数内部修改的是b引用地址上的值,所以外部也改变了
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:输出 `1` 和 `100` 都对,核心原因也说对了。更严谨一点可以记成:
|
||||||
|
- `int` 传入方法时改的是副本
|
||||||
|
- 数组传入方法时传的是引用的副本,但这个副本仍然指向同一个数组对象,所以能改到内部元素
|
||||||
|
|
||||||
|
### 8. [简答题] ref 和 out
|
||||||
|
根据你的笔记,总结 `ref` 和 `out` 的区别,至少写 3 点。然后再补充一个你认为适合用 `out` 的使用场景。
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.ref在使用时必须在外部赋值,out在传入前不能赋值,或赋值和传出值无关
|
||||||
|
2.相反out在函数内部必须赋值,ref可以不
|
||||||
|
3.out流向只输出,ref是双向
|
||||||
|
|
||||||
|
out的应用场景可以是TryParse,将某个类型转换为int类型而int默认情况下(不加?修饰)不能为null,所以必须用out在TryParse方法体内返回一个值出来
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- `ref` 传入前必须先初始化,这点你答对了
|
||||||
|
- `out` 传入前是“可以不初始化”,不是“不能初始化”
|
||||||
|
- `out` 在方法结束前必须赋值,这点你答对了
|
||||||
|
- 把 `out` 理解成“额外带回一个结果”也对,`TryParse` 的例子很合适
|
||||||
|
|
||||||
|
补充:这里的关键不是“`int` 不能为 `null`”,而是方法除了返回“是否成功”之外,还要再返回一个解析出来的值,所以适合用 `out`
|
||||||
|
|
||||||
|
### 9. [找错题] out 参数
|
||||||
|
下面代码能否通过编译?如果不能,请指出原因。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
void Test(ref int a, out int b)
|
||||||
|
{
|
||||||
|
a++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = 1;
|
||||||
|
int y;
|
||||||
|
Test(ref x, out y);
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
不能,out传递的参数在函数结束时没有赋值
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:问题抓得很准,`out int b` 在方法结束前没有赋值,所以代码不能通过编译。
|
||||||
|
|
||||||
|
### 10. [应用题] 变长参数
|
||||||
|
请根据你的笔记,回答下面两个问题:
|
||||||
|
|
||||||
|
1. `params` 变长参数有哪些规则?
|
||||||
|
2. 请写出一个方法签名,使它能够接收任意个 `int` 参数,并返回它们的和。只写方法签名也可以,愿意的话也可以顺手把方法体写出来。
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.只能作用于数组对象,必须放在方法参数列表的末尾
|
||||||
|
2.访问修饰符 static int Sum(params int[] arr){}
|
||||||
|
具体里面是一个循环了arr.Length次的数组,逐次取出并累加到一个变量里并最终将他返回
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:你答到了两条关键规则:
|
||||||
|
- `params` 后面必须是数组
|
||||||
|
- `params` 参数必须放在参数列表末尾
|
||||||
|
|
||||||
|
还差一条:
|
||||||
|
- 一个方法里最多只能有一个 `params` 参数
|
||||||
|
|
||||||
|
你写的方法签名 `static int Sum(params int[] arr)` 是正确的。
|
||||||
|
|
||||||
|
### 11. [代码阅读题] 枚举
|
||||||
|
阅读下面代码并回答问题:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
enum E_PlayerState
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Move,
|
||||||
|
Attack = 10,
|
||||||
|
Die
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
1. `Idle`、`Move`、`Attack`、`Die` 对应的整数值分别是多少?
|
||||||
|
2. 为什么笔记里说枚举和 `switch` “天生一对”?
|
||||||
|
3. 为什么一般不推荐把枚举量写成中文?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.0、1、10、11
|
||||||
|
2.因为switch的case可以识别传递的枚举类型,或直接用枚举的"下标"值作为对象
|
||||||
|
3.中文在编译时容易报错,兼容性不好
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:基本正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 第 1 问数值全对:`0、1、10、11`
|
||||||
|
- 第 2 问意思基本对,可以再补一句:用枚举配合 `switch` 时,`case` 可以直接按状态名写,代码比直接写数字更清晰
|
||||||
|
- 第 3 问建议改成“命名规范、可读性和兼容性更好”,而不是说“中文一定会编译报错”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心
|
||||||
|
|
||||||
|
### 12. [简答题] 继承和 base
|
||||||
|
根据你的笔记,回答下面问题:
|
||||||
|
|
||||||
|
1. `Dog : Animal` 表达的是什么关系?
|
||||||
|
2. 为什么子类里通常可以访问父类的 `protected` 成员,但不能直接访问 `private` 成员?
|
||||||
|
3. `base(name)` 的作用是什么?
|
||||||
|
4. 为什么说“构造函数不会被继承”?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.Dog继承Animal的关系
|
||||||
|
2.因为private修饰的成员只有当前类自己才能访问,而protect修饰的成员是当前或继承该类的子类都可以访问
|
||||||
|
3.base(name)意思是继承的父类的name成员
|
||||||
|
4.因为首先违反了命名规则,如果子类继承了父类的构造函数,那么函数名和子类对不上,其次重名之后程序会不知道调用的是子类的构造函数还是父类的构造函数
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 第 1 问和第 2 问基本对
|
||||||
|
- ==第 3 问答错了,`base(name)` 的作用是“调用父类构造函数,并把 `name` 传给父类==”
|
||||||
|
- 第 4 问的更准确说法是:构造函数属于各自类型,不会被子类直接继承,子类只是可以通过 `base(...)` 去调用父类构造函数
|
||||||
|
|
||||||
|
==补一句你这题最该记住的话:`base(name)` 不是“继承父类成员”,而是“调用父类构造函数”。==
|
||||||
|
|
||||||
|
### 13. [代码阅读题] 静态成员
|
||||||
|
阅读下面代码,写出输出结果,并解释为什么这里更适合用 `Player.Count` 来访问。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
public static int Count;
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Player(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Player p1 = new Player("A");
|
||||||
|
Player p2 = new Player("B");
|
||||||
|
|
||||||
|
Console.WriteLine(Player.Count);
|
||||||
|
```
|
||||||
|
|
||||||
|
然后再补充回答:
|
||||||
|
|
||||||
|
1. 静态成员和实例成员最核心的区别是什么?
|
||||||
|
2. `const` 和 `static readonly` 的区别是什么?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
因为static int了count成员后,所有player类实例化对象在操作count时只会操作同一个对象,这样可以统一一个值,如果不是static的话每个player实例化对象都会操作自己的count,反而对不上总人数了。
|
||||||
|
1.静态成员首先在程序调用到时会自动创建内存并一直存在直到程序结束不会被GC清理,实例成员如果没有引用后可以被GC清理,静态成员只有唯一一个地址/标识,但实例成员可以是被实例化成很多地址/成员
|
||||||
|
2.const和static readonly的区别是const必须在声明时赋值,static readonly可以先声明,后续在构造函数中赋值
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 你对“为什么适合用 `Player.Count`”的理解是对的:它是所有对象共享的一份数据
|
||||||
|
- 但这题少写了最直接的一问:输出结果是 `2`
|
||||||
|
- “静态成员一直存在直到程序结束不会被 GC 清理”这个说法太绝对了,先不要这么记
|
||||||
|
|
||||||
|
更稳的记法:
|
||||||
|
- 静态成员属于类型本身
|
||||||
|
- 实例成员属于具体对象
|
||||||
|
- `const` 必须声明时赋值,属于编译期常量
|
||||||
|
- `static readonly` 可以在声明时赋值,也可以在静态构造函数中赋值,属于运行期确定
|
||||||
|
|
||||||
|
### 14. [代码编写题] 成员属性
|
||||||
|
请补全下面这个类中的 `Age` 属性,要求:
|
||||||
|
|
||||||
|
1. 外部可以读和写
|
||||||
|
2. 当赋值小于 0 时,内部实际保存为 0
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
private int age;
|
||||||
|
|
||||||
|
// 请在这里补全 Age 属性
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
```csharp
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
private int age;
|
||||||
|
|
||||||
|
public int Age
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if(value < 0) age = 0;
|
||||||
|
age = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:有错误,需要修改。
|
||||||
|
|
||||||
|
点评:你的 `get` 写对了,但 `set` 里有逻辑漏洞:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if(value < 0) age = 0;
|
||||||
|
age = value;
|
||||||
|
```
|
||||||
|
|
||||||
|
这样即使 `value < 0`,下一行还是会把负数重新赋给 `age`,所以最终并不会变成 `0`。
|
||||||
|
|
||||||
|
更正后可以写成:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public int Age
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
age = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
age = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15. [代码编写题] 索引器
|
||||||
|
请根据你的笔记,给下面这个类补一个索引器,让外部可以通过 `team[0]` 这种方式读写成员名。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Team
|
||||||
|
{
|
||||||
|
private string[] members = new string[3];
|
||||||
|
|
||||||
|
// 请在这里补全索引器
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
补充回答:
|
||||||
|
|
||||||
|
1. 为什么索引器的名字固定写成 `this`?
|
||||||
|
2. 为什么说它“很像属性,但访问器带参数”?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
```csharp
|
||||||
|
class Team
|
||||||
|
{
|
||||||
|
private string[] members = new string[3];
|
||||||
|
|
||||||
|
public string this[int index]{get{return members[index];}}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
1.表示该类本身
|
||||||
|
2.因为它可以get或set,但访问需要下标
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:你已经把索引器最核心的语法写出来了,也知道它为什么像属性。但题目要求“可以读写成员名”,所以这里只写 `get` 还不够,还需要补 `set`。
|
||||||
|
|
||||||
|
参考写法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public string this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return members[index];
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
members[index] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 16. [综合设计题] 小型角色类
|
||||||
|
请你自己设计一个简单的 `Player` 或 `Student` 类,要求至少用到下面 4 个知识点中的 3 个:
|
||||||
|
|
||||||
|
- 枚举
|
||||||
|
- 继承
|
||||||
|
- 静态成员
|
||||||
|
- 属性
|
||||||
|
- 索引器
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. 写出核心代码
|
||||||
|
2. 简单说明你每一部分分别用到了什么知识点
|
||||||
|
3. 如果你不想写完整代码,也可以写类结构和关键成员
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
```Csharp
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Exercise2
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 学生年级枚举。
|
||||||
|
/// </summary>
|
||||||
|
enum StudentLevel
|
||||||
|
{
|
||||||
|
Freshman,
|
||||||
|
Sophomore,
|
||||||
|
Junior,
|
||||||
|
Senior
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示三门课程成绩的值类型。
|
||||||
|
/// </summary>
|
||||||
|
struct Score
|
||||||
|
{
|
||||||
|
public int Chinese;
|
||||||
|
public int Math;
|
||||||
|
public int English;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 三门成绩总分。
|
||||||
|
/// </summary>
|
||||||
|
public int Total
|
||||||
|
{
|
||||||
|
get { return Chinese + Math + English; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化成绩。
|
||||||
|
/// </summary>
|
||||||
|
public Score(int chinese, int math, int english)
|
||||||
|
{
|
||||||
|
Chinese = chinese;
|
||||||
|
Math = math;
|
||||||
|
English = english;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重载 + 运算符,用于合并两个成绩对象。
|
||||||
|
/// </summary>
|
||||||
|
public static Score operator +(Score left, Score right)
|
||||||
|
{
|
||||||
|
return new Score(
|
||||||
|
left.Chinese + right.Chinese,
|
||||||
|
left.Math + right.Math,
|
||||||
|
left.English + right.English
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 学生基类。
|
||||||
|
/// </summary>
|
||||||
|
class Student
|
||||||
|
{
|
||||||
|
public const int PassLine = 180;
|
||||||
|
|
||||||
|
private int _id;
|
||||||
|
private string _name;
|
||||||
|
private int _age;
|
||||||
|
private Score _score;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计已创建的学生对象总数。
|
||||||
|
/// </summary>
|
||||||
|
public static int StudentCount { get; private set; }
|
||||||
|
|
||||||
|
public int Id
|
||||||
|
{
|
||||||
|
get { return _id; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public StudentLevel Level { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 学生姓名属性。
|
||||||
|
/// </summary>
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get { return _name; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_name = string.IsNullOrWhiteSpace(value) ? "未命名" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 学生年龄属性,年龄不能小于 0。
|
||||||
|
/// </summary>
|
||||||
|
public int Age
|
||||||
|
{
|
||||||
|
get { return _age; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("年龄不能小于 0。");
|
||||||
|
}
|
||||||
|
|
||||||
|
_age = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 学生成绩属性,三门成绩都不能小于 0。
|
||||||
|
/// </summary>
|
||||||
|
public Score Score
|
||||||
|
{
|
||||||
|
get { return _score; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value.Chinese < 0 || value.Math < 0 || value.English < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("成绩不能小于 0。");
|
||||||
|
}
|
||||||
|
|
||||||
|
_score = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化学生对象。
|
||||||
|
/// </summary>
|
||||||
|
public Student(int id, string name, int age, Score score, StudentLevel level)
|
||||||
|
{
|
||||||
|
_id = id;
|
||||||
|
Name = name;
|
||||||
|
Age = age;
|
||||||
|
Score = score;
|
||||||
|
Level = level;
|
||||||
|
StudentCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 输出学生信息。
|
||||||
|
/// </summary>
|
||||||
|
public virtual void PrintInfo()
|
||||||
|
{
|
||||||
|
Console.WriteLine("============================================");
|
||||||
|
Console.WriteLine($"学号:{Id}");
|
||||||
|
Console.WriteLine($"姓名:{Name}");
|
||||||
|
Console.WriteLine($"年龄:{Age}");
|
||||||
|
Console.WriteLine($"年级:{Level}");
|
||||||
|
Console.WriteLine($"语文:{Score.Chinese}");
|
||||||
|
Console.WriteLine($"数学:{Score.Math}");
|
||||||
|
Console.WriteLine($"英语:{Score.English}");
|
||||||
|
Console.WriteLine($"总分:{Score.Total}");
|
||||||
|
Console.WriteLine(Score.Total >= PassLine ? "结果:及格" : "结果:不及格");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 研究生类,继承自 Student。
|
||||||
|
/// </summary>
|
||||||
|
class GraduateStudent : Student
|
||||||
|
{
|
||||||
|
public string ResearchDirection { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化研究生对象,并调用父类构造函数。
|
||||||
|
/// </summary>
|
||||||
|
public GraduateStudent(
|
||||||
|
int id,
|
||||||
|
string name,
|
||||||
|
int age,
|
||||||
|
Score score,
|
||||||
|
StudentLevel level,
|
||||||
|
string researchDirection)
|
||||||
|
: base(id, name, age, score, level)
|
||||||
|
{
|
||||||
|
ResearchDirection = string.IsNullOrWhiteSpace(researchDirection) ? "未定方向" : researchDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 输出研究生信息。
|
||||||
|
/// </summary>
|
||||||
|
public override void PrintInfo()
|
||||||
|
{
|
||||||
|
base.PrintInfo();
|
||||||
|
Console.WriteLine($"研究方向:{ResearchDirection}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 班级管理类。
|
||||||
|
/// </summary>
|
||||||
|
class ClassRoom
|
||||||
|
{
|
||||||
|
private const int MaxStudentCount = 40;
|
||||||
|
private readonly Student[] _students = new Student[MaxStudentCount];
|
||||||
|
private int _count = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前班级已添加学生数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get { return _count; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 索引器:通过下标访问学生对象。
|
||||||
|
/// </summary>
|
||||||
|
public Student this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _count)
|
||||||
|
{
|
||||||
|
throw new IndexOutOfRangeException("索引超出范围。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _students[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一次添加多个学生。
|
||||||
|
/// </summary>
|
||||||
|
public void AddStudents(params Student[] students)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < students.Length; i++)
|
||||||
|
{
|
||||||
|
if (_count >= MaxStudentCount)
|
||||||
|
{
|
||||||
|
Console.WriteLine("班级已满,无法继续添加学生。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_students[_count] = students[i];
|
||||||
|
_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按学号查找学生。
|
||||||
|
/// </summary>
|
||||||
|
public bool FindStudent(int id, out Student result)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _count; i++)
|
||||||
|
{
|
||||||
|
if (_students[i].Id == id)
|
||||||
|
{
|
||||||
|
result = _students[i];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 给某门成绩加分,演示 ref 的使用。
|
||||||
|
/// </summary>
|
||||||
|
public static void AddBonus(ref int score, int bonus)
|
||||||
|
{
|
||||||
|
score += bonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 程序入口。
|
||||||
|
/// </summary>
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Student s1 = new Student(
|
||||||
|
1,
|
||||||
|
"小周",
|
||||||
|
18,
|
||||||
|
new Score(90, 95, 85),
|
||||||
|
StudentLevel.Freshman
|
||||||
|
);
|
||||||
|
|
||||||
|
Student s2 = new Student(
|
||||||
|
2,
|
||||||
|
"小王",
|
||||||
|
19,
|
||||||
|
new Score(80, 85, 90),
|
||||||
|
StudentLevel.Sophomore
|
||||||
|
);
|
||||||
|
|
||||||
|
GraduateStudent g1 = new GraduateStudent(
|
||||||
|
3,
|
||||||
|
"小李",
|
||||||
|
24,
|
||||||
|
new Score(88, 90, 92),
|
||||||
|
StudentLevel.Senior,
|
||||||
|
"人工智能"
|
||||||
|
);
|
||||||
|
|
||||||
|
ClassRoom classRoom = new ClassRoom();
|
||||||
|
classRoom.AddStudents(s1, s2, g1);
|
||||||
|
|
||||||
|
Console.WriteLine("索引器访问第 2 个学生:");
|
||||||
|
Console.WriteLine(classRoom[1].Name);
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("按学号查找学生:");
|
||||||
|
if (classRoom.FindStudent(2, out Student result))
|
||||||
|
{
|
||||||
|
result.PrintInfo();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("没找到该学生。");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("给小周的语文成绩加 10 分:");
|
||||||
|
Score updatedScore = s1.Score;
|
||||||
|
ClassRoom.AddBonus(ref updatedScore.Chinese, 10);
|
||||||
|
s1.Score = updatedScore;
|
||||||
|
Console.WriteLine($"加分后语文:{s1.Score.Chinese}");
|
||||||
|
Console.WriteLine($"加分后总分:{s1.Score.Total}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine($"当前已创建的学生对象总数:{Student.StudentCount}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Score totalScore = s1.Score + s2.Score;
|
||||||
|
Console.WriteLine("s1 + s2 的成绩合并结果:");
|
||||||
|
Console.WriteLine($"语文合计:{totalScore.Chinese}");
|
||||||
|
Console.WriteLine($"数学合计:{totalScore.Math}");
|
||||||
|
Console.WriteLine($"英语合计:{totalScore.English}");
|
||||||
|
Console.WriteLine($"总分合计:{totalScore.Total}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("普通学生信息:");
|
||||||
|
s1.PrintInfo();
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("研究生信息:");
|
||||||
|
g1.PrintInfo();
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("说明:");
|
||||||
|
Console.WriteLine("Score 适合用 struct,因为它只是一个小型成绩数据组合,拷贝成本低,语义上更像值。");
|
||||||
|
Console.WriteLine("Student 适合用 class,因为学生包含身份信息,通常需要被多个地方共享和持续修改。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:优秀。
|
||||||
|
|
||||||
|
点评:这题完成度很高,已经明显超出“只要能覆盖 3 个知识点”的要求了。你实际用到了:
|
||||||
|
- 枚举
|
||||||
|
- 继承
|
||||||
|
- 静态成员
|
||||||
|
- 属性
|
||||||
|
- 索引器
|
||||||
|
- 甚至还额外用了 `struct`、运算符重载、`params`、`ref`、`out`
|
||||||
|
|
||||||
|
优点:
|
||||||
|
- 类之间职责比较清楚
|
||||||
|
- `Student` / `GraduateStudent` 的继承关系自然
|
||||||
|
- `StudentCount`、属性封装、索引器都写得比较完整
|
||||||
|
- 能把多个知识点串起来,说明你不是只会背概念,而是在尝试组织代码结构
|
||||||
|
|
||||||
|
小建议:
|
||||||
|
- 如果你想让 `ClassRoom` 的索引器更完整,也可以再补一个 `set`
|
||||||
|
- `null!`、运算符重载这些已经超出当前这套题的核心范围了,不过写出来说明你有主动扩展
|
||||||
|
|
||||||
|
这道题我给高评价,思路和完成度都很好。
|
||||||
@@ -0,0 +1,795 @@
|
|||||||
|
# CSharp 综合练习 02
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
- 本题单基于你当前已经完成的 C# 笔记整理。
|
||||||
|
- 本次重点覆盖:
|
||||||
|
- 上一套练习里的易错点回收
|
||||||
|
- `多态`
|
||||||
|
- `抽象类和抽象方法`
|
||||||
|
- `接口`
|
||||||
|
- `密封类`
|
||||||
|
- `密封函数`
|
||||||
|
- `命名空间`
|
||||||
|
- `抽象类和接口的区别`
|
||||||
|
- 你可以直接在每道题下方的 `你的作答` 区域填写答案。
|
||||||
|
- 我后续会继续在 `批改区` 直接给你批改和点评。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一部分:错题回收
|
||||||
|
|
||||||
|
### 1. [分类题] 格式转换再辨析
|
||||||
|
请把下面每一行代码分别归类为:
|
||||||
|
|
||||||
|
- 隐式转换
|
||||||
|
- 显式强制转换
|
||||||
|
- 转成字符串
|
||||||
|
- 以上都不是
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
int a = 10;
|
||||||
|
double b = a;
|
||||||
|
int c = (int)12.8;
|
||||||
|
string s = 123.ToString();
|
||||||
|
bool ok = int.TryParse("123", out int value);
|
||||||
|
```
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. 给第 2~5 行分别分类
|
||||||
|
2. 用一句话说明 `Parse` / `TryParse` 为什么常用于“字符串转数字”
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
```
|
||||||
|
2.隐式转换
|
||||||
|
3.显式强制转换
|
||||||
|
4.转成字符串
|
||||||
|
5.以上都不是
|
||||||
|
`Parse` 本来就是专门把字符串内容解析成目标数字类型的方法,所以特别常用于“字符串转数字”
|
||||||
|
```
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:第 2~5 行的分类都对,`Parse / TryParse` 的用途说明也准确。你这题已经把“强转”“转字符串”“解析字符串”区分开了。
|
||||||
|
|
||||||
|
### 2. [简答题] ref 和 out 精准区分
|
||||||
|
请至少写出 `ref` 和 `out` 的 3 个区别,并额外回答下面两个判断:
|
||||||
|
|
||||||
|
1. `out` 参数在传入前能不能已经有值?
|
||||||
|
2. `TryParse` 这种写法为什么更适合用 `out` 而不是 `ref`?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
ref和out的区别
|
||||||
|
1.ref必须在传入前赋值
|
||||||
|
2.out必须在传出前赋值
|
||||||
|
|
||||||
|
1.out传入前可以有值
|
||||||
|
2.因为out传出前必须赋值,更符合TryParse的应用情景
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 你答对了两点:`ref` 传入前必须先赋值,`out` 在方法结束前必须赋值
|
||||||
|
- `out` 传入前“可以有值”,这点你也答对了
|
||||||
|
- 但题目要求至少写 3 个区别,你这里只写出了 2 个核心点
|
||||||
|
|
||||||
|
建议补上这一句:
|
||||||
|
- `ref` 更像“把已有变量传进去继续读写”
|
||||||
|
- `out` 更像“额外带回一个结果”
|
||||||
|
|
||||||
|
`TryParse` 适合 `out` 的原因你答到了主线:它除了返回“是否成功”,还要把解析结果再带出来。
|
||||||
|
|
||||||
|
### 3. [应用题] params 规则回收
|
||||||
|
根据你的笔记,回答下面问题:
|
||||||
|
|
||||||
|
1. `params` 参数有哪些规则?
|
||||||
|
2. 下面两个方法签名,哪个是合法的?为什么?
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
void Test1(params int[] arr, int x)
|
||||||
|
void Test2(int x, params int[] arr)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 一个方法里能不能出现两个 `params` 参数?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.params的规则有1.只能修饰数组类型对象,2.必须放在函数的参数列表末尾,3.一个函数只能有一格变长参数
|
||||||
|
2.第二个是合法的,因为第一个没有把变长数组放在参数列表的末尾
|
||||||
|
3.不能,这样的写法是不合法的
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:三条规则都答对了:
|
||||||
|
- `params` 后面必须是数组
|
||||||
|
- 必须放在参数列表末尾
|
||||||
|
- 一个方法里最多只能有一个 `params`
|
||||||
|
|
||||||
|
`Test2` 合法、`Test1` 不合法,这里判断没有问题。
|
||||||
|
|
||||||
|
### 4. [代码阅读题] base 的真正作用
|
||||||
|
阅读下面代码并回答问题:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Animal(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Console.WriteLine("Animal 构造");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public Dog(string name) : base(name)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Dog 构造");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dog d = new Dog("旺财");
|
||||||
|
Console.WriteLine(d.Name);
|
||||||
|
```
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
1. `base(name)` 的作用是什么?
|
||||||
|
2. 代码执行时,哪一句先输出?
|
||||||
|
3. 为什么说这里不是“继承了父类的构造函数”?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.base(name)是复用父对象的构造函数来给name对象初始化
|
||||||
|
2.先输出父对象的cw
|
||||||
|
3.没有为什么,就不是继承,而是调用父类构造函数
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 你知道 `base(name)` 和父类构造函数有关,这个方向是对的
|
||||||
|
- 但更准确的说法是:`base(name)` 是“调用父类构造函数,并把 `name` 传过去”
|
||||||
|
- 这里先输出的是 `Animal 构造`
|
||||||
|
|
||||||
|
第 3 问你结论对,但解释太短了。更完整的记法是:
|
||||||
|
- 构造函数不会被继承
|
||||||
|
- 子类只是可以在自己的构造函数里用 `base(...)` 调用父类构造函数
|
||||||
|
|
||||||
|
### 5. [代码阅读题] 静态成员本质
|
||||||
|
阅读下面代码,写出输出结果,并回答问题:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
public static int Count;
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
public Player(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Player p1 = new Player("A");
|
||||||
|
Player p2 = new Player("B");
|
||||||
|
|
||||||
|
Console.WriteLine(Player.Count);
|
||||||
|
```
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
1. 最终输出什么?
|
||||||
|
2. 为什么这里更适合通过 `Player.Count` 访问,而不是让每个对象自己保存一份人数?
|
||||||
|
3. 静态成员和实例成员最核心的区别是什么?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.2
|
||||||
|
2.自己保管自己的容易出现数量不一样的问题,都通过一个对象管理更统一
|
||||||
|
3.静态成员有唯一性,实例成员可以有很多
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:输出 `2`,原因分析也对。这里最核心的一句可以记成:
|
||||||
|
- 静态成员属于类本身,所有对象共享
|
||||||
|
- 实例成员属于具体对象,每个对象各有一份
|
||||||
|
|
||||||
|
### 6. [改错题] 属性的 set 逻辑
|
||||||
|
下面这个属性写法有逻辑错误。请指出错误,并改成正确版本。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public int Age
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
age = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
age = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
set最终会被赋值为value,if的逻辑被忽略了
|
||||||
|
改正版本就是往里加个return
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:你已经抓到错误本质了,问题确实在于最后还是执行了 `age = value;`,把前面的判断覆盖掉了。
|
||||||
|
|
||||||
|
不过这题要求“改成正确版本”,所以最好把代码完整写出来,例如:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public int Age
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
age = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
age = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. [代码编写题] 索引器补全
|
||||||
|
请补全下面类中的索引器,要求外部既可以读,也可以写:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Team
|
||||||
|
{
|
||||||
|
private string[] members = new string[3];
|
||||||
|
|
||||||
|
// 请补全索引器
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
补充回答:
|
||||||
|
|
||||||
|
1. 为什么索引器名字固定写 `this`?
|
||||||
|
2. 为什么它“很像属性,但访问器带参数”?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
```Csharp
|
||||||
|
public string this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return members[index];
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
memebers[index] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
1.因为规定的语法就是这么写的,别再问这么傻逼的问题了,或者说代表自己这个类
|
||||||
|
2.因为他要给别人用所以必须带参数不带参数怎么知道要访问的是哪个
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 结构写对了,`get / set` 也都补上了
|
||||||
|
- 但代码里有一个拼写错误:`memebers` 应该是 `members`,不然会编译报错
|
||||||
|
|
||||||
|
补充回答里,比较稳的表述可以记成:
|
||||||
|
- 索引器名字固定写 `this`,这是语法规定,表示当前对象支持 `[]` 访问
|
||||||
|
- 它像属性,是因为也有 `get / set`
|
||||||
|
- 它又和普通属性不同,因为访问时要带索引参数,比如 `team[0]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第二部分:核心专题
|
||||||
|
|
||||||
|
### 8. [简答题] 多态的核心句子
|
||||||
|
根据你的笔记,用自己的话解释下面几件事:
|
||||||
|
|
||||||
|
1. 什么是多态?
|
||||||
|
2. `virtual`、`override`、`base` 分别起什么作用?
|
||||||
|
3. “编译时看变量类型,运行时看实际对象” 这句话是什么意思?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.父对象在子对象中,同样的行为有不同的表现
|
||||||
|
2.virtual表示这是个虚方法,可以在子类中被重写,override表示这是个重写的方法,base表示访问父类成员
|
||||||
|
3.如果我有两个类Person和Player是继承关系,都有SayHi方法,如果我声明时用父类,装父类,用父类方法出来的是父类方法,子类也是同理,但是父类装子类时,再用SayHi就会用父类方法而不是具体的子类方法,重写能规避这个问题
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 你知道多态是在“同样调用下表现不同”,方向是对的
|
||||||
|
- `virtual / override / base` 的作用也基本对
|
||||||
|
|
||||||
|
但第 3 问你那句“父类装子类时,再用 SayHi 就会用父类方法”是错的。
|
||||||
|
如果父类方法是 `virtual`,子类正确 `override` 了,那么父类引用指向子类对象时,调用的是子类重写后的版本。
|
||||||
|
|
||||||
|
这题最稳的记法是:
|
||||||
|
- 编译时看变量类型:决定你“能调用哪些成员”
|
||||||
|
- 运行时看实际对象:决定你“真正执行哪个重写版本”
|
||||||
|
|
||||||
|
### 9. [代码阅读题] override 和 new 的区别
|
||||||
|
阅读下面两段代码,分别写出输出结果,并说明为什么不同。
|
||||||
|
|
||||||
|
第一段:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Father
|
||||||
|
{
|
||||||
|
public virtual void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Father");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Son : Father
|
||||||
|
{
|
||||||
|
public override void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Son");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Father f = new Son();
|
||||||
|
f.Say();
|
||||||
|
```
|
||||||
|
|
||||||
|
第二段:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Father
|
||||||
|
{
|
||||||
|
public void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Father");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Son : Father
|
||||||
|
{
|
||||||
|
public new void Say()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Son");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Father f = new Son();
|
||||||
|
f.Say();
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.Son因为重写了
|
||||||
|
2.Father因为没重写,且容器是父类所以还是在用父类方法
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:第一段输出 `Son`,第二段输出 `Father`,原因你也说到了:
|
||||||
|
- `override` 是真正的多态重写
|
||||||
|
- `new` 只是隐藏父类同名成员,不是多态重写
|
||||||
|
|
||||||
|
### 10. [判断题] 密封类和密封函数
|
||||||
|
请判断下面说法是对还是错,如果错了请改正:
|
||||||
|
|
||||||
|
1. `sealed class` 表示这个类不能再被继承
|
||||||
|
2. `sealed override` 会让当前类也不能再被继承
|
||||||
|
3. 普通方法可以直接写成 `sealed void Test()`
|
||||||
|
4. 密封类不能创建对象
|
||||||
|
5. 密封类和抽象类不能同时使用
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.T
|
||||||
|
2.F,只是方法不能被重写了,不会不让类被继续继承
|
||||||
|
3.F,都没重写,不能直接加sealed
|
||||||
|
4.F,可以创建对象,只是不能再被继承了
|
||||||
|
5.T
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:五个判断都对。你已经把:
|
||||||
|
- `sealed class`
|
||||||
|
- `sealed override`
|
||||||
|
- 普通方法不能直接 `sealed`
|
||||||
|
- 密封类仍可创建对象
|
||||||
|
- `sealed` 和 `abstract` 不能同时用
|
||||||
|
|
||||||
|
这几个点区分清楚了。
|
||||||
|
|
||||||
|
### 11. [代码阅读题] sealed override
|
||||||
|
阅读下面代码,判断哪些地方合法,哪些地方不合法,并说明原因。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Animal
|
||||||
|
{
|
||||||
|
public virtual void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("动物叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public sealed override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("狗叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Husky : Dog
|
||||||
|
{
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("哈士奇叫");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
补充回答:
|
||||||
|
|
||||||
|
1. `sealed override` 和 `sealed class` 的区别是什么?
|
||||||
|
2. `Dog` 类本身还能不能被继承?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
Husky类中父类已经把Speak Sealed了,不能继续重写
|
||||||
|
1.前者只是保证该方法不会被继续重写,后者是保证整个类不被继续继承
|
||||||
|
2.可以,类本身没被Sealed
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:你抓住了关键点:`Husky` 里那段 `override Speak()` 不合法,因为 `Dog.Speak()` 已经被 `sealed override` 封住了。
|
||||||
|
|
||||||
|
补充两问你也答对了:
|
||||||
|
- `sealed override` 只封成员
|
||||||
|
- `sealed class` 封整个类
|
||||||
|
- `Dog` 本身还能继续被继承
|
||||||
|
|
||||||
|
### 12. [简答题] 抽象类和抽象方法
|
||||||
|
根据你的笔记,回答下面问题:
|
||||||
|
|
||||||
|
1. 抽象类为什么不能直接 `new`?
|
||||||
|
2. 抽象方法和普通方法最核心的区别是什么?
|
||||||
|
3. 抽象类里能不能有构造函数、普通方法、字段、属性?
|
||||||
|
4. 普通子类继承抽象类后,必须做什么?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.因为规则就是这样的,abstract类可以被理解为模板,是抽象的,不是具体的
|
||||||
|
2.普通类是具体的可以直接实例化对象,抽象类必须先通过重写才能有实例
|
||||||
|
3.可以有,只要子类去实现具体实例就可以了
|
||||||
|
4.必须重写抽象类中声明的构造函数、方法、字段等
|
||||||
|
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 第 3 问答对了,抽象类里可以有构造函数、普通方法、字段、属性
|
||||||
|
- 第 1 问你的理解接近了,但更准确地说:抽象类是不完整类型,可能还包含没实现的抽象成员,所以不能直接 `new`
|
||||||
|
- 第 2 问答偏了。题目问的是“抽象方法和普通方法”,不是“抽象类和普通类”
|
||||||
|
- 第 4 问答错了。普通子类必须实现的是“所有抽象成员”,不是“重写构造函数、字段等”
|
||||||
|
|
||||||
|
建议你把这题压缩记成:
|
||||||
|
- 抽象方法没有方法体
|
||||||
|
- 普通方法有方法体
|
||||||
|
- 普通子类继承抽象类后,必须 `override` 所有抽象成员
|
||||||
|
|
||||||
|
### 13. [代码编写题] 抽象类
|
||||||
|
请根据你的笔记,补全下面代码:
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. `Animal` 是抽象类
|
||||||
|
2. `Speak()` 是抽象方法
|
||||||
|
3. `Dog` 继承 `Animal`
|
||||||
|
4. `Dog` 正确实现 `Speak()`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 请补全
|
||||||
|
```
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
```Csharp
|
||||||
|
abstract class Animal
|
||||||
|
{
|
||||||
|
public abstract void Speak();
|
||||||
|
}
|
||||||
|
class Dog : Animal
|
||||||
|
{
|
||||||
|
public override void Speak()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Woof!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:代码满足题目要求:
|
||||||
|
- `Animal` 是抽象类
|
||||||
|
- `Speak()` 是抽象方法
|
||||||
|
- `Dog` 继承 `Animal`
|
||||||
|
- `Dog` 正确 `override` 了 `Speak()`
|
||||||
|
|
||||||
|
这题写得干净,没问题。
|
||||||
|
|
||||||
|
### 14. [简答题] 接口
|
||||||
|
根据你的笔记,回答下面问题:
|
||||||
|
|
||||||
|
1. 接口主要表达的是什么?
|
||||||
|
2. 为什么接口更适合表示“能力”而不是“身份”?
|
||||||
|
3. 一个类能继承几个类?能实现几个接口?
|
||||||
|
4. 为什么实现接口成员时通常要写 `public`?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.表达某个可以实现的功能,或者如你2中所说的能力
|
||||||
|
2.身份通常是唯一的,能力可以是多重的,如果把“能唱歌”当成身份(`Singer`作为抽象类),那么某个学生毕业成为程序员后可能不再是`Singer`,但实际他依然能唱歌。用接口则灵活保持这个能力。
|
||||||
|
3.一个类只能继承一个类,而接口没有数量限制
|
||||||
|
4.写成private外部无法调用,且违背原则编译错误
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:这题答得比较完整。
|
||||||
|
- 你对“接口表达能力而不是身份”的理解是到位的
|
||||||
|
- 第 3 问也对:类只能继承一个父类,但可以实现多个接口
|
||||||
|
- 第 4 问也对:实现接口成员时访问级别不能比接口契约更低
|
||||||
|
|
||||||
|
### 15. [代码编写题] 接口实现
|
||||||
|
请完成下面代码,让 `Bird` 和 `Plane` 都能通过同一个方法被调用:
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. 定义一个 `IFlyable` 接口
|
||||||
|
2. `Bird` 和 `Plane` 都实现它
|
||||||
|
3. 写一个 `LetItFly(IFlyable flyable)` 方法
|
||||||
|
4. 在方法里调用飞行行为
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 请补全
|
||||||
|
```
|
||||||
|
|
||||||
|
```Csharp
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
interface IFlyable
|
||||||
|
{
|
||||||
|
public void LetItFly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Flying...");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Bird : IFlyable
|
||||||
|
{
|
||||||
|
public Bird()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public void LetItFly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Bird Flying... ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Plane : IFlyable
|
||||||
|
{
|
||||||
|
public Plane()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public void LetItFly()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Plane Flying... ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
public static void LetItFly(IFlyable flyable)
|
||||||
|
{
|
||||||
|
flyable.LetItFly();
|
||||||
|
}
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
IFlyable f1 = new Bird();
|
||||||
|
IFlyable f2 = new Plane();
|
||||||
|
|
||||||
|
LetItFly(f1);
|
||||||
|
LetItFly(f2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:部分正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 你的代码确实达成了“Bird 和 Plane 能通过同一个方法统一调用”的目标
|
||||||
|
- `LetItFly(IFlyable flyable)` 这部分也写出来了
|
||||||
|
|
||||||
|
但按这套笔记的学习范围,更标准的写法应该是:
|
||||||
|
- 接口里只声明方法,不写方法体
|
||||||
|
- 接口方法名通常写成 `Fly()`,外部统一调用方法再写成 `LetItFly(IFlyable flyable)`
|
||||||
|
|
||||||
|
也就是说,更推荐的结构是:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
interface IFlyable
|
||||||
|
{
|
||||||
|
void Fly();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 `Bird` / `Plane` 里实现 `Fly()`。
|
||||||
|
你这题功能上基本成立,但接口的“契约”味道还可以再更清晰一点。
|
||||||
|
|
||||||
|
### 16. [代码阅读题] 接口多实现
|
||||||
|
阅读下面代码,并回答问题:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
interface IMovable
|
||||||
|
{
|
||||||
|
void Move();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttackable
|
||||||
|
{
|
||||||
|
void Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player : IMovable, IAttackable
|
||||||
|
{
|
||||||
|
public void Move()
|
||||||
|
{
|
||||||
|
Console.WriteLine("玩家移动");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Attack()
|
||||||
|
{
|
||||||
|
Console.WriteLine("玩家攻击");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
1. 这段代码说明了接口的什么特点?
|
||||||
|
2. 如果 `Player` 只实现了 `Move()`,没实现 `Attack()`,会怎么样?
|
||||||
|
3. 为什么说接口可以弥补“类只能单继承”的限制?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.面向能力编程
|
||||||
|
2.报错,继承了必须实现
|
||||||
|
3.因为接口让你给一个类“附加”多种不相关的行为能力,而这些行为无法(或不适合)放进单条继承链里。
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:三问都答对了。
|
||||||
|
- 这段代码体现了“一个类可以实现多个接口”
|
||||||
|
- 少实现一个接口成员会直接报错
|
||||||
|
- 接口可以给一个类补充多种能力,这是它对单继承限制的重要补充
|
||||||
|
|
||||||
|
### 17. [简答题] 命名空间
|
||||||
|
根据你的笔记,回答下面问题:
|
||||||
|
|
||||||
|
1. `namespace` 的作用是什么?
|
||||||
|
2. `using` 的作用是什么?
|
||||||
|
3. 如果 `Game` 和 `Tool` 两个命名空间里都有 `Player`,该怎么区分使用?
|
||||||
|
4. 命名空间和访问修饰符是不是一回事?为什么?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.将程序划分为不同的区域,防止命名冲突
|
||||||
|
2.using的作用就和include一样,引用命名空间
|
||||||
|
3.Game.Player和Tool.Player
|
||||||
|
4.命名空间用于组织代码逻辑分组(解决命名冲突),访问修饰符用于控制成员的可访问性(封装);前者是“在哪”,后者是“谁能用”。
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:正确。
|
||||||
|
|
||||||
|
点评:这题比较稳。
|
||||||
|
- `namespace` 负责组织代码、避免重名
|
||||||
|
- `using` 负责引入命名空间,简化书写
|
||||||
|
- 同名类型可以用完整限定名区分
|
||||||
|
- 命名空间和访问修饰符不是一回事
|
||||||
|
|
||||||
|
你最后那句“前者是在哪,后者是谁能用”总结得很好。
|
||||||
|
|
||||||
|
### 18. [对比题] 抽象类和接口的区别
|
||||||
|
请根据你的笔记,从下面几个角度对比抽象类和接口:
|
||||||
|
|
||||||
|
1. 它们分别更适合表达什么?
|
||||||
|
2. 在“共同代码复用”方面,谁更适合?
|
||||||
|
3. 在“一个类拥有多种能力”方面,谁更适合?
|
||||||
|
4. 什么情况下更适合用抽象类,什么情况下更适合用接口?
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
1.一个是用来表达某个东西的通用模板,一个用来表达一个通用的方法
|
||||||
|
2.抽象类
|
||||||
|
3.接口
|
||||||
|
4.打个比方:鸟和飞机都能飞,但他们不属于一类东西,飞这个功能是都有的,所以用接口。人和猩猩都是猿,且都有猿类独有的特征如直立行走等所以可以用猿类抽象类
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:基本正确。
|
||||||
|
|
||||||
|
点评:
|
||||||
|
- 第 2、3、4 问都答到了重点
|
||||||
|
- 第 1 问再严谨一点可以写成:
|
||||||
|
- 抽象类更适合表达“是什么”
|
||||||
|
- 接口更适合表达“能做什么 / 具备什么能力”
|
||||||
|
|
||||||
|
你举的“鸟和飞机都会飞”这个例子是合适的,已经能说明接口为什么更适合表达能力。
|
||||||
|
|
||||||
|
### 19. [综合设计题] 角色系统小练习
|
||||||
|
请你自己设计一个简单的角色系统,要求至少用到下面 6 个知识点中的 4 个:
|
||||||
|
|
||||||
|
- 继承
|
||||||
|
- 多态
|
||||||
|
- 抽象类
|
||||||
|
- 接口
|
||||||
|
- 静态成员
|
||||||
|
- 属性
|
||||||
|
- 索引器
|
||||||
|
- 密封函数
|
||||||
|
- 命名空间
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. 写出核心代码
|
||||||
|
2. 标出你分别用到了哪些知识点
|
||||||
|
3. 简单说明为什么这里用抽象类,为什么这里用接口
|
||||||
|
4. 如果你愿意,也可以顺手加上一个你自己的小扩展
|
||||||
|
|
||||||
|
**你的作答**
|
||||||
|
(待填写)
|
||||||
|
|
||||||
|
**批改区**
|
||||||
|
判定:良好。
|
||||||
|
|
||||||
|
点评:我按你提供的 `D:\Visual Studio\Program\ReStudy CSharp\Restudy\Exercise3` 代码一起批改了。
|
||||||
|
|
||||||
|
这套代码里你实际用到了很多要求内的知识点:
|
||||||
|
- 继承:`Player : Character`、`Enemy : Character`、`Car : Vehicle`
|
||||||
|
- 多态:`Character[]` 中装 `Player` 和 `Enemy`,统一调用 `Move()` / `Attack()`
|
||||||
|
- 抽象类:`Character`、`Vehicle`
|
||||||
|
- 接口:`IDrivable`
|
||||||
|
- 静态成员:`playerCount`、`enemyCount`
|
||||||
|
- 属性:`maxPassenger`、`passengers`、`location`
|
||||||
|
- 索引器:`Vehicle.this[int index]`
|
||||||
|
- 密封函数:`Car` 中的 `sealed override Drive(...)`
|
||||||
|
- 命名空间:`Program`、`Character`、`Vehicle`、`Vector`
|
||||||
|
|
||||||
|
优点:
|
||||||
|
- 结构已经有“小项目”味道了,不只是单题代码
|
||||||
|
- 你能把抽象类、多态、接口、静态成员、索引器串起来用,这点很好
|
||||||
|
- `Character[]` 统一调度角色行为,这个设计是对路的
|
||||||
|
|
||||||
|
还可以继续改进的点:
|
||||||
|
- 你在题目里没有把“为什么这里用抽象类、为什么这里用接口”写成文字说明,这一部分算缺失
|
||||||
|
- `IAttackable` 和 `IMovable` 文件目前没有真正接入 `Character / Player / Enemy` 体系,像是还没用上的草稿接口
|
||||||
|
- `IDrivable.Drive(Player player)` 这个接口写得稍微有点窄,如果以后不是只有 `Player` 能开车,这个接口会不够通用
|
||||||
|
|
||||||
|
综合看,这题已经明显达到要求,而且超出了“只用 4 个知识点”的下限。解释部分如果补得更完整,这题会更强。
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# ArrayList
|
||||||
|
概念:
|
||||||
|
`ArrayList` 是 C# 提供的一个==非泛型集合类==
|
||||||
|
本质上可以理解成:==一个长度可变的 `object` 数组==
|
||||||
|
|
||||||
|
它可以:
|
||||||
|
1. 像数组一样按下标访问
|
||||||
|
2. 自动扩容
|
||||||
|
3. 增删查改元素
|
||||||
|
4. 存储任意类型的对象
|
||||||
|
|
||||||
|
命名空间:
|
||||||
|
```csharp
|
||||||
|
using System.Collections;
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
是 `System.Collections`
|
||||||
|
不是 `System.Collection`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 通常用法
|
||||||
|
1. 学习 C# 早期非泛型集合
|
||||||
|
2. 理解 `object`、装箱和拆箱
|
||||||
|
3. 存储数量不固定的数据
|
||||||
|
4. 对数组进行更方便的增删操作
|
||||||
|
|
||||||
|
实际开发中:
|
||||||
|
新代码更推荐使用 `List<T>`
|
||||||
|
`ArrayList` 主要作为进阶阶段理解集合历史和装箱拆箱的材料
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
创建:
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
```
|
||||||
|
|
||||||
|
添加:
|
||||||
|
```csharp
|
||||||
|
list.Add(数据);
|
||||||
|
```
|
||||||
|
|
||||||
|
访问:
|
||||||
|
```csharp
|
||||||
|
list[下标]
|
||||||
|
```
|
||||||
|
|
||||||
|
长度:
|
||||||
|
```csharp
|
||||||
|
list.Count
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
|
||||||
|
list.Add("小明");
|
||||||
|
list.Add(18);
|
||||||
|
list.Add(true);
|
||||||
|
|
||||||
|
Console.WriteLine(list[0]);
|
||||||
|
Console.WriteLine(list[1]);
|
||||||
|
Console.WriteLine(list[2]);
|
||||||
|
Console.WriteLine(list.Count);
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `ArrayList` 内部存的是 `object`
|
||||||
|
2. 所以字符串、整数、布尔值都可以放进去
|
||||||
|
3. `Count` 表示当前实际存了多少个元素
|
||||||
|
|
||||||
|
----
|
||||||
|
# 增删查改
|
||||||
|
增加:
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
|
||||||
|
list.Add("A");
|
||||||
|
list.Add("B");
|
||||||
|
list.Insert(1, "C");
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
```text
|
||||||
|
A C B
|
||||||
|
```
|
||||||
|
|
||||||
|
删除:
|
||||||
|
```csharp
|
||||||
|
list.Remove("A"); // 删除第一个匹配的元素
|
||||||
|
list.RemoveAt(0); // 删除指定下标的元素
|
||||||
|
list.Clear(); // 清空所有元素
|
||||||
|
```
|
||||||
|
|
||||||
|
查找:
|
||||||
|
```csharp
|
||||||
|
bool hasB = list.Contains("B");
|
||||||
|
int index = list.IndexOf("B");
|
||||||
|
```
|
||||||
|
|
||||||
|
修改:
|
||||||
|
```csharp
|
||||||
|
list[0] = "新的值";
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 遍历
|
||||||
|
使用 `for`:
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
list.Add("A");
|
||||||
|
list.Add("B");
|
||||||
|
list.Add("C");
|
||||||
|
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
Console.WriteLine(list[i]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 `foreach`:
|
||||||
|
```csharp
|
||||||
|
foreach (object item in list)
|
||||||
|
{
|
||||||
|
Console.WriteLine(item);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因为 `ArrayList` 中的元素都是按 `object` 看待,所以 `foreach` 变量常写成 `object`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 取值时要类型转换
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
list.Add(100);
|
||||||
|
|
||||||
|
int num = (int)list[0];
|
||||||
|
Console.WriteLine(num);
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
`list[0]` 的返回类型是 `object`
|
||||||
|
如果要当成 `int` 使用,就需要拆箱 / 强制类型转换
|
||||||
|
|
||||||
|
错误示意:
|
||||||
|
```csharp
|
||||||
|
int num = list[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
这样会报错,因为不能把 `object` 直接赋值给 `int`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 装箱和拆箱问题
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
|
||||||
|
int hp = 100;
|
||||||
|
list.Add(hp); // int 放进 object,发生装箱
|
||||||
|
|
||||||
|
int value = (int)list[0]; // object 取回 int,发生拆箱
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. 值类型放进 `ArrayList` 时,会发生装箱
|
||||||
|
2. 从 `ArrayList` 取出值类型时,需要拆箱
|
||||||
|
3. 装箱和拆箱都有额外开销
|
||||||
|
4. 类型转错还会运行时报错
|
||||||
|
|
||||||
|
这个点可以和[[万物之父和装箱与拆箱|装箱与拆箱]]一起理解。
|
||||||
|
|
||||||
|
----
|
||||||
|
# Capacity 和 Count
|
||||||
|
`Count`:
|
||||||
|
当前实际有多少个元素
|
||||||
|
|
||||||
|
`Capacity`:
|
||||||
|
内部数组当前能容纳多少个元素
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
|
||||||
|
Console.WriteLine(list.Count);
|
||||||
|
Console.WriteLine(list.Capacity);
|
||||||
|
|
||||||
|
list.Add("A");
|
||||||
|
list.Add("B");
|
||||||
|
|
||||||
|
Console.WriteLine(list.Count);
|
||||||
|
Console.WriteLine(list.Capacity);
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
`ArrayList` 内部会自动扩容
|
||||||
|
`Count` 是实际数量
|
||||||
|
`Capacity` 是容量,不一定等于数量
|
||||||
|
|
||||||
|
----
|
||||||
|
# 排序和反转
|
||||||
|
排序:
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
list.Add(3);
|
||||||
|
list.Add(1);
|
||||||
|
list.Add(2);
|
||||||
|
|
||||||
|
list.Sort();
|
||||||
|
```
|
||||||
|
|
||||||
|
反转:
|
||||||
|
```csharp
|
||||||
|
list.Reverse();
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
如果里面混放了不能互相比较的类型,`Sort()` 可能会出错
|
||||||
|
|
||||||
|
例如:
|
||||||
|
```csharp
|
||||||
|
list.Add(10);
|
||||||
|
list.Add("A");
|
||||||
|
list.Sort(); // 可能出错
|
||||||
|
```
|
||||||
|
|
||||||
|
所以虽然 `ArrayList` 可以存任意类型,但最好不要随便混放。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和数组的区别
|
||||||
|
| 对比点 | 数组 | ArrayList |
|
||||||
|
| ---- | --------- | -------------- |
|
||||||
|
| 长度 | 创建后固定 | 可以动态增删 |
|
||||||
|
| 类型 | 通常固定一种类型 | 内部按 `object` 存 |
|
||||||
|
| 访问 | 可以用下标 | 可以用下标 |
|
||||||
|
| 性能 | 类型明确,性能较好 | 可能有装箱拆箱 |
|
||||||
|
| 常用程度 | 基础常用 | 现在新项目较少用 |
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见错误
|
||||||
|
错误1:忘记引用命名空间
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
```
|
||||||
|
|
||||||
|
解决:
|
||||||
|
```csharp
|
||||||
|
using System.Collections;
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
错误2:取值时忘记强转
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
list.Add(10);
|
||||||
|
|
||||||
|
int num = list[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
应该写:
|
||||||
|
```csharp
|
||||||
|
int num = (int)list[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
错误3:混放类型后强转错误
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
list.Add("100");
|
||||||
|
|
||||||
|
int num = (int)list[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
`list[0]` 实际是字符串,不是整数,会出错。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. `ArrayList` 在 `System.Collections` 命名空间中
|
||||||
|
2. `ArrayList` 是非泛型集合,内部按 `object` 存储
|
||||||
|
3. 它可以动态增删元素
|
||||||
|
4. 取出元素时通常需要强制类型转换
|
||||||
|
5. 值类型放入 `ArrayList` 会发生装箱
|
||||||
|
6. 不建议随意混放不同类型,容易产生转换错误
|
||||||
|
7. 新项目通常优先使用 `List<T>`,类型更安全
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==ArrayList 是会自动扩容的 object 数组,方便但不够类型安全。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“ArrayList 是 `System.Collections` 命名空间下的非泛型集合,本质上可以理解为动态扩容的 `object` 数组。
|
||||||
|
它可以存储任意类型,但取值时通常需要强制类型转换,值类型放进去还会发生装箱。
|
||||||
|
所以它适合用来理解非泛型集合和装箱拆箱,新项目中一般更推荐使用泛型集合 `List<T>`。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [ArrayList Class](https://learn.microsoft.com/en-us/dotnet/api/system.collections.arraylist?view=net-9.0)
|
||||||
|
2. Microsoft Learn: [System.Collections Namespace](https://learn.microsoft.com/en-us/dotnet/api/system.collections?view=net-9.0)
|
||||||
|
|
||||||
|
#ArrayList
|
||||||
|
#集合
|
||||||
|
#进阶
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# Hashtable
|
||||||
|
概念:
|
||||||
|
`Hashtable` 是 C# 提供的一个==非泛型键值对集合==
|
||||||
|
它通过键 `key` 来查找值 `value`
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
像一本字典
|
||||||
|
用“词”查“解释”
|
||||||
|
用 `key` 查 `value`
|
||||||
|
|
||||||
|
命名空间:
|
||||||
|
```csharp
|
||||||
|
using System.Collections;
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 通常用法
|
||||||
|
1. 保存键值对数据
|
||||||
|
2. 通过唯一键快速查找对应值
|
||||||
|
3. 学习哈希表这种数据结构
|
||||||
|
4. 理解 `Dictionary<TKey, TValue>` 之前的非泛型写法
|
||||||
|
|
||||||
|
实际开发中:
|
||||||
|
新代码更推荐使用 `Dictionary<TKey, TValue>`
|
||||||
|
`Hashtable` 主要用于学习非泛型键值对集合
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
创建:
|
||||||
|
```csharp
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
```
|
||||||
|
|
||||||
|
添加:
|
||||||
|
```csharp
|
||||||
|
table.Add(键, 值);
|
||||||
|
```
|
||||||
|
|
||||||
|
通过键取值:
|
||||||
|
```csharp
|
||||||
|
object value = table[键];
|
||||||
|
```
|
||||||
|
|
||||||
|
修改:
|
||||||
|
```csharp
|
||||||
|
table[键] = 新值;
|
||||||
|
```
|
||||||
|
|
||||||
|
数量:
|
||||||
|
```csharp
|
||||||
|
table.Count
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
|
||||||
|
table.Add("name", "小明");
|
||||||
|
table.Add("age", 18);
|
||||||
|
table.Add("isVip", true);
|
||||||
|
|
||||||
|
Console.WriteLine(table["name"]);
|
||||||
|
Console.WriteLine(table["age"]);
|
||||||
|
Console.WriteLine(table["isVip"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `"name"`、`"age"`、`"isVip"` 是键
|
||||||
|
2. `"小明"`、`18`、`true` 是值
|
||||||
|
3. 通过键可以找到对应的值
|
||||||
|
|
||||||
|
----
|
||||||
|
# Add 和索引器
|
||||||
|
使用 `Add` 添加:
|
||||||
|
```csharp
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
|
||||||
|
table.Add("id", 1001);
|
||||||
|
table.Add("name", "小明");
|
||||||
|
```
|
||||||
|
|
||||||
|
使用索引器添加或修改:
|
||||||
|
```csharp
|
||||||
|
table["score"] = 90; // 如果 key 不存在,就是新增
|
||||||
|
table["score"] = 100; // 如果 key 已存在,就是修改
|
||||||
|
```
|
||||||
|
|
||||||
|
区别:
|
||||||
|
1. `Add` 遇到重复键会报错
|
||||||
|
2. `table[key] = value` 遇到重复键会覆盖旧值
|
||||||
|
|
||||||
|
----
|
||||||
|
# 增删查改
|
||||||
|
增加:
|
||||||
|
```csharp
|
||||||
|
table.Add("level", 10);
|
||||||
|
```
|
||||||
|
|
||||||
|
删除:
|
||||||
|
```csharp
|
||||||
|
table.Remove("level");
|
||||||
|
table.Clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
查找键:
|
||||||
|
```csharp
|
||||||
|
bool hasName = table.ContainsKey("name");
|
||||||
|
```
|
||||||
|
|
||||||
|
查找值:
|
||||||
|
```csharp
|
||||||
|
bool hasValue = table.ContainsValue("小明");
|
||||||
|
```
|
||||||
|
|
||||||
|
修改:
|
||||||
|
```csharp
|
||||||
|
table["name"] = "小红";
|
||||||
|
```
|
||||||
|
|
||||||
|
取值:
|
||||||
|
```csharp
|
||||||
|
string name = (string)table["name"];
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 遍历
|
||||||
|
遍历键值对:
|
||||||
|
```csharp
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
|
||||||
|
table.Add("name", "小明");
|
||||||
|
table.Add("age", 18);
|
||||||
|
|
||||||
|
foreach (DictionaryEntry item in table)
|
||||||
|
{
|
||||||
|
Console.WriteLine(item.Key + " : " + item.Value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
只遍历键:
|
||||||
|
```csharp
|
||||||
|
foreach (object key in table.Keys)
|
||||||
|
{
|
||||||
|
Console.WriteLine(key);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
只遍历值:
|
||||||
|
```csharp
|
||||||
|
foreach (object value in table.Values)
|
||||||
|
{
|
||||||
|
Console.WriteLine(value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
`Hashtable` 的遍历顺序不保证等于添加顺序
|
||||||
|
|
||||||
|
----
|
||||||
|
# key 不能重复
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
|
||||||
|
table.Add("name", "小明");
|
||||||
|
table.Add("name", "小红");
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
`Hashtable` 中的 key 必须唯一
|
||||||
|
重复添加同一个 key 会报错
|
||||||
|
|
||||||
|
如果想修改旧值:
|
||||||
|
```csharp
|
||||||
|
table["name"] = "小红";
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# key 不能是 null
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
|
||||||
|
table.Add(null, "值");
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
`Hashtable` 的 key 不能为 `null`
|
||||||
|
|
||||||
|
但是 value 可以是 `null`:
|
||||||
|
```csharp
|
||||||
|
table.Add("data", null);
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 取值时要判断 key 是否存在
|
||||||
|
```csharp
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
table.Add("name", "小明");
|
||||||
|
|
||||||
|
if (table.ContainsKey("name"))
|
||||||
|
{
|
||||||
|
string name = (string)table["name"];
|
||||||
|
Console.WriteLine(name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
为什么要判断:
|
||||||
|
1. key 不存在时,`table[key]` 可能拿到 `null`
|
||||||
|
2. value 本身也可能是 `null`
|
||||||
|
3. 用 `ContainsKey` 可以区分“没有这个键”和“这个键的值是 null”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 取值时要类型转换
|
||||||
|
```csharp
|
||||||
|
Hashtable table = new Hashtable();
|
||||||
|
table.Add("age", 18);
|
||||||
|
|
||||||
|
int age = (int)table["age"];
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
非泛型 `Hashtable` 的 key 和 value 都是按 `object` 处理
|
||||||
|
取出来后通常需要强制类型转换
|
||||||
|
|
||||||
|
如果类型转错:
|
||||||
|
```csharp
|
||||||
|
string age = (string)table["age"];
|
||||||
|
```
|
||||||
|
|
||||||
|
会出错,因为 `"age"` 对应的值实际是 `int`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 和 ArrayList 的区别
|
||||||
|
| 对比点 | ArrayList | Hashtable |
|
||||||
|
|---|---|---|
|
||||||
|
| 存储方式 | 单个元素 | 键值对 |
|
||||||
|
| 访问方式 | 通过下标 | 通过 key |
|
||||||
|
| key 是否存在 | 没有 key | key 必须唯一 |
|
||||||
|
| 常见用途 | 普通列表 | 字典式查找 |
|
||||||
|
| 泛型替代 | `List<T>` | `Dictionary<TKey, TValue>` |
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见错误
|
||||||
|
错误1:重复 Add 同一个 key
|
||||||
|
```csharp
|
||||||
|
table.Add("id", 1);
|
||||||
|
table.Add("id", 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
应该改成:
|
||||||
|
```csharp
|
||||||
|
table["id"] = 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
错误2:以为 Hashtable 有添加顺序
|
||||||
|
|
||||||
|
实际:
|
||||||
|
`Hashtable` 是根据 key 的哈希来组织数据
|
||||||
|
遍历顺序不保证是添加顺序
|
||||||
|
|
||||||
|
----
|
||||||
|
错误3:取值后忘记强转
|
||||||
|
```csharp
|
||||||
|
int age = table["age"];
|
||||||
|
```
|
||||||
|
|
||||||
|
应该写:
|
||||||
|
```csharp
|
||||||
|
int age = (int)table["age"];
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. `Hashtable` 在 `System.Collections` 命名空间中
|
||||||
|
2. `Hashtable` 是非泛型键值对集合
|
||||||
|
3. key 不能重复
|
||||||
|
4. key 不能是 `null`
|
||||||
|
5. value 可以是 `null`
|
||||||
|
6. `Add` 重复 key 会报错
|
||||||
|
7. `table[key] = value` 可以新增,也可以覆盖旧值
|
||||||
|
8. 遍历顺序不保证等于添加顺序
|
||||||
|
9. 新项目通常优先使用 `Dictionary<TKey, TValue>`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==Hashtable 是用 key 找 value 的非泛型键值对集合。==
|
||||||
|
|
||||||
|
再压缩一点:
|
||||||
|
==ArrayList 靠下标找,Hashtable 靠 key 找。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“Hashtable 是 `System.Collections` 命名空间下的非泛型键值对集合,通过唯一 key 查找 value。
|
||||||
|
它的 key 不能重复,也不能为 null;value 可以为 null。
|
||||||
|
因为 key 和 value 都按 object 处理,所以取值时通常需要强制类型转换。新项目中一般更推荐使用泛型集合 `Dictionary<TKey, TValue>`。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [Hashtable Class](https://learn.microsoft.com/dotnet/api/system.collections.hashtable?view=net-9.0)
|
||||||
|
2. Microsoft Learn: [Hashtable.Add Method](https://learn.microsoft.com/en-us/dotnet/api/system.collections.hashtable.add?view=net-9.0)
|
||||||
|
3. Microsoft Learn: [Hashtable.Item Property](https://learn.microsoft.com/en-us/dotnet/api/system.collections.hashtable.item?view=net-9.0)
|
||||||
|
4. Microsoft Learn: [System.Collections Namespace](https://learn.microsoft.com/en-us/dotnet/api/system.collections?view=net-9.0)
|
||||||
|
|
||||||
|
#Hashtable
|
||||||
|
#哈希表
|
||||||
|
#集合
|
||||||
|
#进阶
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
# Queue
|
||||||
|
概念:
|
||||||
|
`Queue` 是 C# 提供的一个==非泛型队列集合==
|
||||||
|
队列的特点是:==先进先出==
|
||||||
|
|
||||||
|
英文:
|
||||||
|
`FIFO`:First In First Out
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
像排队买东西
|
||||||
|
先排队的人,先被处理
|
||||||
|
|
||||||
|
命名空间:
|
||||||
|
```csharp
|
||||||
|
using System.Collections;
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 通常用法
|
||||||
|
1. 处理需要“按进入顺序依次处理”的数据
|
||||||
|
2. 保存等待处理的任务
|
||||||
|
3. 模拟排队、消息、请求等逻辑
|
||||||
|
4. 学习队列这种数据结构
|
||||||
|
|
||||||
|
实际开发中:
|
||||||
|
新代码更推荐使用 `Queue<T>`
|
||||||
|
这里的 `Queue` 是非泛型版本
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
创建:
|
||||||
|
```csharp
|
||||||
|
Queue queue = new Queue();
|
||||||
|
```
|
||||||
|
|
||||||
|
入队:
|
||||||
|
```csharp
|
||||||
|
queue.Enqueue(数据);
|
||||||
|
```
|
||||||
|
|
||||||
|
出队:
|
||||||
|
```csharp
|
||||||
|
object value = queue.Dequeue();
|
||||||
|
```
|
||||||
|
|
||||||
|
查看队头:
|
||||||
|
```csharp
|
||||||
|
object value = queue.Peek();
|
||||||
|
```
|
||||||
|
|
||||||
|
数量:
|
||||||
|
```csharp
|
||||||
|
queue.Count
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
Queue queue = new Queue();
|
||||||
|
|
||||||
|
queue.Enqueue("第一个任务");
|
||||||
|
queue.Enqueue("第二个任务");
|
||||||
|
queue.Enqueue("第三个任务");
|
||||||
|
|
||||||
|
Console.WriteLine(queue.Dequeue());
|
||||||
|
Console.WriteLine(queue.Dequeue());
|
||||||
|
Console.WriteLine(queue.Dequeue());
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:
|
||||||
|
```text
|
||||||
|
第一个任务
|
||||||
|
第二个任务
|
||||||
|
第三个任务
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `"第一个任务"` 最先进队列
|
||||||
|
2. `Dequeue()` 会先取出最早进入队列的元素
|
||||||
|
3. 这就是先进先出
|
||||||
|
|
||||||
|
----
|
||||||
|
# Enqueue、Dequeue、Peek
|
||||||
|
`Enqueue`:
|
||||||
|
把元素添加到队尾
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
queue.Enqueue("A");
|
||||||
|
```
|
||||||
|
|
||||||
|
`Dequeue`:
|
||||||
|
取出并删除队头元素
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
object item = queue.Dequeue();
|
||||||
|
```
|
||||||
|
|
||||||
|
`Peek`:
|
||||||
|
查看队头元素,但不删除
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
object item = queue.Peek();
|
||||||
|
```
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
Queue queue = new Queue();
|
||||||
|
queue.Enqueue("A");
|
||||||
|
queue.Enqueue("B");
|
||||||
|
|
||||||
|
Console.WriteLine(queue.Peek()); // A
|
||||||
|
Console.WriteLine(queue.Count); // 2
|
||||||
|
|
||||||
|
Console.WriteLine(queue.Dequeue()); // A
|
||||||
|
Console.WriteLine(queue.Count); // 1
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 遍历
|
||||||
|
```csharp
|
||||||
|
Queue queue = new Queue();
|
||||||
|
|
||||||
|
queue.Enqueue("A");
|
||||||
|
queue.Enqueue("B");
|
||||||
|
queue.Enqueue("C");
|
||||||
|
|
||||||
|
foreach (object item in queue)
|
||||||
|
{
|
||||||
|
Console.WriteLine(item);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出顺序一般是:
|
||||||
|
```text
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
遍历 `Queue` 时,通常按入队顺序从队头到队尾遍历
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常用成员
|
||||||
|
```csharp
|
||||||
|
Queue queue = new Queue();
|
||||||
|
|
||||||
|
queue.Enqueue("A");
|
||||||
|
queue.Enqueue("B");
|
||||||
|
|
||||||
|
Console.WriteLine(queue.Count);
|
||||||
|
Console.WriteLine(queue.Contains("A"));
|
||||||
|
|
||||||
|
object first = queue.Peek();
|
||||||
|
object value = queue.Dequeue();
|
||||||
|
|
||||||
|
queue.Clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
常用方法:
|
||||||
|
1. `Enqueue(object obj)`:入队
|
||||||
|
2. `Dequeue()`:出队并删除
|
||||||
|
3. `Peek()`:查看队头但不删除
|
||||||
|
4. `Contains(object obj)`:判断是否包含
|
||||||
|
5. `Clear()`:清空
|
||||||
|
6. `ToArray()`:转成数组
|
||||||
|
|
||||||
|
----
|
||||||
|
# 空队列不能 Dequeue 或 Peek
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
Queue queue = new Queue();
|
||||||
|
|
||||||
|
queue.Dequeue();
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
队列里没有元素,不能取队头
|
||||||
|
|
||||||
|
更稳的写法:
|
||||||
|
```csharp
|
||||||
|
if (queue.Count > 0)
|
||||||
|
{
|
||||||
|
object value = queue.Dequeue();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Peek()` 也一样,使用前最好先判断 `Count`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 取值时要类型转换
|
||||||
|
```csharp
|
||||||
|
Queue queue = new Queue();
|
||||||
|
queue.Enqueue(100);
|
||||||
|
|
||||||
|
int num = (int)queue.Dequeue();
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
非泛型 `Queue` 中的元素按 `object` 存储
|
||||||
|
取出来也是 `object`
|
||||||
|
|
||||||
|
值类型入队和出队时,也会涉及装箱拆箱:
|
||||||
|
```csharp
|
||||||
|
queue.Enqueue(100); // 装箱
|
||||||
|
int num = (int)queue.Dequeue(); // 拆箱
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 适合场景:任务队列
|
||||||
|
```csharp
|
||||||
|
Queue taskQueue = new Queue();
|
||||||
|
|
||||||
|
taskQueue.Enqueue("加载配置");
|
||||||
|
taskQueue.Enqueue("读取存档");
|
||||||
|
taskQueue.Enqueue("进入场景");
|
||||||
|
|
||||||
|
while (taskQueue.Count > 0)
|
||||||
|
{
|
||||||
|
string task = (string)taskQueue.Dequeue();
|
||||||
|
Console.WriteLine("处理:" + task);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
任务按照加入顺序依次处理
|
||||||
|
这种情况适合用队列
|
||||||
|
|
||||||
|
----
|
||||||
|
# Queue 和 Stack 的区别
|
||||||
|
| 对比点 | Queue | Stack |
|
||||||
|
|---|---|---|
|
||||||
|
| 规则 | 先进先出 | 后进先出 |
|
||||||
|
| 英文 | FIFO | LIFO |
|
||||||
|
| 添加 | `Enqueue` | `Push` |
|
||||||
|
| 取出 | `Dequeue` | `Pop` |
|
||||||
|
| 查看 | `Peek` 查看队头 | `Peek` 查看栈顶 |
|
||||||
|
| 适合场景 | 排队处理任务 | 撤销、回退 |
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
排队用 `Queue`
|
||||||
|
回退用 `Stack`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. `Queue` 在 `System.Collections` 命名空间中
|
||||||
|
2. `Queue` 是非泛型集合,元素按 `object` 存储
|
||||||
|
3. 队列的规则是先进先出
|
||||||
|
4. `Enqueue` 是入队
|
||||||
|
5. `Dequeue` 是出队并删除队头
|
||||||
|
6. `Peek` 是查看队头但不删除
|
||||||
|
7. 空队列调用 `Dequeue()` 或 `Peek()` 会出错
|
||||||
|
8. 新项目通常优先使用 `Queue<T>`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==Queue 是先进先出的集合,先进去的先出来。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“Queue 是队列结构,特点是先进先出,也就是 FIFO。
|
||||||
|
常用方法有 `Enqueue`、`Dequeue` 和 `Peek`,分别表示入队、出队、查看队头。
|
||||||
|
非泛型 Queue 存的是 object,取值时可能需要强制类型转换,新项目中更推荐使用 `Queue<T>`。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [Queue Class](https://learn.microsoft.com/en-us/dotnet/api/system.collections.queue?view=net-9.0)
|
||||||
|
2. Microsoft Learn: [Queue.Enqueue Method](https://learn.microsoft.com/en-us/dotnet/api/system.collections.queue.enqueue?view=net-9.0)
|
||||||
|
3. Microsoft Learn: [System.Collections Namespace](https://learn.microsoft.com/en-us/dotnet/api/system.collections?view=net-9.0)
|
||||||
|
|
||||||
|
#Queue
|
||||||
|
#队列
|
||||||
|
#集合
|
||||||
|
#进阶
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
# Stack
|
||||||
|
概念:
|
||||||
|
`Stack` 是 C# 提供的一个==非泛型栈集合==
|
||||||
|
栈的特点是:==先进后出,后进先出==
|
||||||
|
|
||||||
|
英文:
|
||||||
|
`LIFO`:Last In First Out
|
||||||
|
|
||||||
|
简单理解:
|
||||||
|
像一摞盘子
|
||||||
|
最后放上去的,最先被拿走
|
||||||
|
|
||||||
|
命名空间:
|
||||||
|
```csharp
|
||||||
|
using System.Collections;
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 通常用法
|
||||||
|
1. 处理需要“后进先出”的数据
|
||||||
|
2. 临时保存操作记录
|
||||||
|
3. 实现撤销、回退、调用顺序等逻辑
|
||||||
|
4. 学习栈这种数据结构
|
||||||
|
|
||||||
|
实际开发中:
|
||||||
|
新代码更推荐使用 `Stack<T>`
|
||||||
|
这里的 `Stack` 是非泛型版本,主要用于理解集合和数据结构
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
创建:
|
||||||
|
```csharp
|
||||||
|
Stack stack = new Stack();
|
||||||
|
```
|
||||||
|
|
||||||
|
入栈:
|
||||||
|
```csharp
|
||||||
|
stack.Push(数据);
|
||||||
|
```
|
||||||
|
|
||||||
|
出栈:
|
||||||
|
```csharp
|
||||||
|
object value = stack.Pop();
|
||||||
|
```
|
||||||
|
|
||||||
|
查看栈顶:
|
||||||
|
```csharp
|
||||||
|
object value = stack.Peek();
|
||||||
|
```
|
||||||
|
|
||||||
|
数量:
|
||||||
|
```csharp
|
||||||
|
stack.Count
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 基础案例
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
Stack stack = new Stack();
|
||||||
|
|
||||||
|
stack.Push("第一步");
|
||||||
|
stack.Push("第二步");
|
||||||
|
stack.Push("第三步");
|
||||||
|
|
||||||
|
Console.WriteLine(stack.Pop());
|
||||||
|
Console.WriteLine(stack.Pop());
|
||||||
|
Console.WriteLine(stack.Pop());
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:
|
||||||
|
```text
|
||||||
|
第三步
|
||||||
|
第二步
|
||||||
|
第一步
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `"第一步"` 最先进栈
|
||||||
|
2. `"第三步"` 最后进栈
|
||||||
|
3. `Pop()` 会先取出最后进去的元素
|
||||||
|
|
||||||
|
----
|
||||||
|
# Push、Pop、Peek
|
||||||
|
`Push`:
|
||||||
|
把元素放到栈顶
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
stack.Push("A");
|
||||||
|
```
|
||||||
|
|
||||||
|
`Pop`:
|
||||||
|
取出并删除栈顶元素
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
object item = stack.Pop();
|
||||||
|
```
|
||||||
|
|
||||||
|
`Peek`:
|
||||||
|
只查看栈顶元素,不删除
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
object item = stack.Peek();
|
||||||
|
```
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
Stack stack = new Stack();
|
||||||
|
stack.Push("A");
|
||||||
|
stack.Push("B");
|
||||||
|
|
||||||
|
Console.WriteLine(stack.Peek()); // B
|
||||||
|
Console.WriteLine(stack.Count); // 2
|
||||||
|
|
||||||
|
Console.WriteLine(stack.Pop()); // B
|
||||||
|
Console.WriteLine(stack.Count); // 1
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 遍历
|
||||||
|
```csharp
|
||||||
|
Stack stack = new Stack();
|
||||||
|
|
||||||
|
stack.Push("A");
|
||||||
|
stack.Push("B");
|
||||||
|
stack.Push("C");
|
||||||
|
|
||||||
|
foreach (object item in stack)
|
||||||
|
{
|
||||||
|
Console.WriteLine(item);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出顺序一般是:
|
||||||
|
```text
|
||||||
|
C
|
||||||
|
B
|
||||||
|
A
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
遍历 `Stack` 时,也是从栈顶到栈底
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常用成员
|
||||||
|
```csharp
|
||||||
|
Stack stack = new Stack();
|
||||||
|
|
||||||
|
stack.Push("A");
|
||||||
|
stack.Push("B");
|
||||||
|
|
||||||
|
Console.WriteLine(stack.Count);
|
||||||
|
Console.WriteLine(stack.Contains("A"));
|
||||||
|
|
||||||
|
object top = stack.Peek();
|
||||||
|
object value = stack.Pop();
|
||||||
|
|
||||||
|
stack.Clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
常用方法:
|
||||||
|
1. `Push(object obj)`:入栈
|
||||||
|
2. `Pop()`:出栈并删除
|
||||||
|
3. `Peek()`:查看栈顶但不删除
|
||||||
|
4. `Contains(object obj)`:判断是否包含
|
||||||
|
5. `Clear()`:清空
|
||||||
|
6. `ToArray()`:转成数组
|
||||||
|
|
||||||
|
----
|
||||||
|
# 空栈不能 Pop 或 Peek
|
||||||
|
错误写法:
|
||||||
|
```csharp
|
||||||
|
Stack stack = new Stack();
|
||||||
|
|
||||||
|
stack.Pop();
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
栈里没有元素,不能取栈顶
|
||||||
|
|
||||||
|
更稳的写法:
|
||||||
|
```csharp
|
||||||
|
if (stack.Count > 0)
|
||||||
|
{
|
||||||
|
object value = stack.Pop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Peek()` 也一样,使用前最好先判断 `Count`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 取值时要类型转换
|
||||||
|
```csharp
|
||||||
|
Stack stack = new Stack();
|
||||||
|
stack.Push(100);
|
||||||
|
|
||||||
|
int num = (int)stack.Pop();
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:
|
||||||
|
非泛型 `Stack` 中的元素按 `object` 存储
|
||||||
|
取出来也是 `object`
|
||||||
|
|
||||||
|
如果值类型放进去,会涉及装箱和拆箱:
|
||||||
|
```csharp
|
||||||
|
stack.Push(100); // 装箱
|
||||||
|
int num = (int)stack.Pop(); // 拆箱
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 适合场景:撤销操作
|
||||||
|
```csharp
|
||||||
|
Stack undoStack = new Stack();
|
||||||
|
|
||||||
|
undoStack.Push("创建角色");
|
||||||
|
undoStack.Push("移动角色");
|
||||||
|
undoStack.Push("修改颜色");
|
||||||
|
|
||||||
|
if (undoStack.Count > 0)
|
||||||
|
{
|
||||||
|
string lastAction = (string)undoStack.Pop();
|
||||||
|
Console.WriteLine("撤销:" + lastAction);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
最后做的操作,通常最先撤销
|
||||||
|
所以适合用栈
|
||||||
|
|
||||||
|
----
|
||||||
|
# Stack 和 ArrayList 的区别
|
||||||
|
| 对比点 | ArrayList | Stack |
|
||||||
|
|---|---|---|
|
||||||
|
| 访问方式 | 按下标访问 | 只关心栈顶 |
|
||||||
|
| 数据顺序 | 按添加顺序保存 | 后进先出 |
|
||||||
|
| 添加方法 | `Add` | `Push` |
|
||||||
|
| 取出方法 | `list[index]` | `Pop` / `Peek` |
|
||||||
|
| 适合场景 | 普通列表 | 回退、撤销、临时反向处理 |
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
1. `Stack` 在 `System.Collections` 命名空间中
|
||||||
|
2. `Stack` 是非泛型集合,元素按 `object` 存储
|
||||||
|
3. 栈的规则是后进先出
|
||||||
|
4. `Push` 是入栈
|
||||||
|
5. `Pop` 是取出并删除栈顶
|
||||||
|
6. `Peek` 是查看栈顶但不删除
|
||||||
|
7. 空栈调用 `Pop()` 或 `Peek()` 会出错
|
||||||
|
8. 新项目通常优先使用 `Stack<T>`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 一句话记忆
|
||||||
|
==Stack 是后进先出的集合,最后放进去的最先出来。==
|
||||||
|
|
||||||
|
----
|
||||||
|
# 面试/复习时怎么说
|
||||||
|
可以这样答:
|
||||||
|
|
||||||
|
“Stack 是栈结构,特点是后进先出,也就是 LIFO。
|
||||||
|
常用方法有 `Push`、`Pop` 和 `Peek`,分别表示入栈、出栈、查看栈顶。
|
||||||
|
非泛型 Stack 存的是 object,取值时可能需要强制类型转换,新项目中更推荐使用 `Stack<T>`。”
|
||||||
|
|
||||||
|
----
|
||||||
|
引用:
|
||||||
|
1. Microsoft Learn: [Stack Class](https://learn.microsoft.com/en-us/dotnet/api/system.collections.stack?view=net-9.0)
|
||||||
|
2. Microsoft Learn: [System.Collections Namespace](https://learn.microsoft.com/en-us/dotnet/api/system.collections?view=net-9.0)
|
||||||
|
|
||||||
|
#Stack
|
||||||
|
#栈
|
||||||
|
#集合
|
||||||
|
#进阶
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
# 泛型
|
||||||
|
|
||||||
|
概念:
|
||||||
|
泛型就是把==类型==参数化,让一份代码可以适配多种数据类型。
|
||||||
|
可以理解为:先把类型空出来,等使用时再指定具体类型。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
```csharp
|
||||||
|
List<int> nums = new List<int>();
|
||||||
|
List<string> names = new List<string>();
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `int` 和 `string` 就是传入泛型的具体类型。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 为什么需要泛型
|
||||||
|
|
||||||
|
泛型的主要作用:
|
||||||
|
1. 提高代码复用性
|
||||||
|
2. 保证类型安全
|
||||||
|
3. 减少强制类型转换
|
||||||
|
4. 避免不必要的装箱和拆箱
|
||||||
|
|
||||||
|
对比 `ArrayList`:
|
||||||
|
```csharp
|
||||||
|
ArrayList list = new ArrayList();
|
||||||
|
list.Add(1);
|
||||||
|
list.Add("hello");
|
||||||
|
|
||||||
|
int num = (int)list[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
`ArrayList` 内部存的是 `object`,取值时经常需要强制转换。
|
||||||
|
|
||||||
|
使用泛型集合:
|
||||||
|
```csharp
|
||||||
|
List<int> list = new List<int>();
|
||||||
|
list.Add(1);
|
||||||
|
|
||||||
|
int num = list[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
`List<int>` 只能存 `int`,编译器会提前帮我们检查类型错误。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 常见泛型形式
|
||||||
|
|
||||||
|
泛型可以用于:
|
||||||
|
1. 泛型类
|
||||||
|
2. 泛型接口
|
||||||
|
3. 泛型方法
|
||||||
|
4. 泛型委托
|
||||||
|
5. 泛型集合
|
||||||
|
|
||||||
|
常见泛型集合:
|
||||||
|
```csharp
|
||||||
|
List<T>
|
||||||
|
Dictionary<TKey, TValue>
|
||||||
|
Queue<T>
|
||||||
|
Stack<T>
|
||||||
|
HashSet<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
`T`、`TKey`、`TValue` 都是泛型类型参数名。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 语法
|
||||||
|
|
||||||
|
泛型类:
|
||||||
|
```csharp
|
||||||
|
class 类名<T>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
泛型接口:
|
||||||
|
```csharp
|
||||||
|
interface 接口名<T>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
泛型方法:
|
||||||
|
```csharp
|
||||||
|
返回值类型 方法名<T>(T 参数)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
多个泛型类型参数:
|
||||||
|
```csharp
|
||||||
|
class 类名<T1, T2>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 泛型类
|
||||||
|
|
||||||
|
泛型类适合用来保存或处理某种不确定类型的数据。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Box<T>
|
||||||
|
{
|
||||||
|
public T Value { get; set; }
|
||||||
|
|
||||||
|
public Box(T value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetValue()
|
||||||
|
{
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用:
|
||||||
|
```csharp
|
||||||
|
Box<int> intBox = new Box<int>(10);
|
||||||
|
Console.WriteLine(intBox.GetValue());
|
||||||
|
|
||||||
|
Box<string> stringBox = new Box<string>("hello");
|
||||||
|
Console.WriteLine(stringBox.GetValue());
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `Box<T>` 定义时不知道 `T` 是什么类型
|
||||||
|
2. `Box<int>` 使用时把 `T` 替换成 `int`
|
||||||
|
3. `Box<string>` 使用时把 `T` 替换成 `string`
|
||||||
|
|
||||||
|
----
|
||||||
|
# 泛型接口
|
||||||
|
|
||||||
|
泛型接口常用于规定一类对象必须提供某种泛型能力。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
interface IRepository<T>
|
||||||
|
{
|
||||||
|
void Add(T item);
|
||||||
|
T Get(int index);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现:
|
||||||
|
```csharp
|
||||||
|
class MemoryRepository<T> : IRepository<T>
|
||||||
|
{
|
||||||
|
private List<T> items = new List<T>();
|
||||||
|
|
||||||
|
public void Add(T item)
|
||||||
|
{
|
||||||
|
items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get(int index)
|
||||||
|
{
|
||||||
|
return items[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用:
|
||||||
|
```csharp
|
||||||
|
IRepository<string> repository = new MemoryRepository<string>();
|
||||||
|
|
||||||
|
repository.Add("小明");
|
||||||
|
Console.WriteLine(repository.Get(0));
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 泛型方法
|
||||||
|
|
||||||
|
泛型方法适合处理“逻辑一样,但参数类型不同”的情况。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static void Print<T>(T value)
|
||||||
|
{
|
||||||
|
Console.WriteLine(value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用:
|
||||||
|
```csharp
|
||||||
|
Print<int>(100);
|
||||||
|
Print<string>("hello");
|
||||||
|
Print<bool>(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
很多时候,编译器可以自动推断泛型类型:
|
||||||
|
```csharp
|
||||||
|
Print(100);
|
||||||
|
Print("hello");
|
||||||
|
Print(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 泛型约束
|
||||||
|
|
||||||
|
默认情况下,泛型类型 `T` 可以是任意类型。
|
||||||
|
|
||||||
|
如果希望限制 `T` 必须满足某些条件,可以使用 `where` 约束。
|
||||||
|
|
||||||
|
语法:
|
||||||
|
```csharp
|
||||||
|
class 类名<T> where T : 约束
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常见约束:
|
||||||
|
|
||||||
|
| 约束 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `where T : class` | `T` 必须是引用类型 |
|
||||||
|
| `where T : struct` | `T` 必须是值类型 |
|
||||||
|
| `where T : new()` | `T` 必须有无参构造函数 |
|
||||||
|
| `where T : 父类名` | `T` 必须继承某个父类 |
|
||||||
|
| `where T : 接口名` | `T` 必须实现某个接口 |
|
||||||
|
|
||||||
|
案例:
|
||||||
|
```csharp
|
||||||
|
class Factory<T> where T : new()
|
||||||
|
{
|
||||||
|
public T Create()
|
||||||
|
{
|
||||||
|
return new T();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因为加了 `where T : new()`,所以可以在类中使用 `new T()`。
|
||||||
|
|
||||||
|
----
|
||||||
|
# 多泛型参数
|
||||||
|
|
||||||
|
一个类或方法可以同时使用多个泛型参数。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Pair<TKey, TValue>
|
||||||
|
{
|
||||||
|
public TKey Key { get; set; }
|
||||||
|
public TValue Value { get; set; }
|
||||||
|
|
||||||
|
public Pair(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用:
|
||||||
|
```csharp
|
||||||
|
Pair<int, string> pair = new Pair<int, string>(1, "小明");
|
||||||
|
|
||||||
|
Console.WriteLine(pair.Key);
|
||||||
|
Console.WriteLine(pair.Value);
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# default 关键字
|
||||||
|
|
||||||
|
泛型中有时不知道 `T` 的默认值是什么,可以使用 `default`。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static T GetDefault<T>()
|
||||||
|
{
|
||||||
|
return default(T);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常见默认值:
|
||||||
|
|
||||||
|
| 类型 | 默认值 |
|
||||||
|
| --- | --- |
|
||||||
|
| `int` | `0` |
|
||||||
|
| `bool` | `false` |
|
||||||
|
| 引用类型 | `null` |
|
||||||
|
| 结构体 | 所有字段都是默认值 |
|
||||||
|
|
||||||
|
新语法也可以写成:
|
||||||
|
```csharp
|
||||||
|
return default;
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
# 完整案例
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace GenericsDemo
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
DataStore<int> intStore = new DataStore<int>();
|
||||||
|
intStore.Add(10);
|
||||||
|
intStore.Add(20);
|
||||||
|
|
||||||
|
Console.WriteLine(intStore.Get(0));
|
||||||
|
Console.WriteLine(intStore.Get(1));
|
||||||
|
|
||||||
|
DataStore<string> stringStore = new DataStore<string>();
|
||||||
|
stringStore.Add("hello");
|
||||||
|
stringStore.Add("world");
|
||||||
|
|
||||||
|
Console.WriteLine(stringStore.Get(0));
|
||||||
|
Console.WriteLine(stringStore.Get(1));
|
||||||
|
|
||||||
|
Print(123);
|
||||||
|
Print("泛型方法");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Print<T>(T value)
|
||||||
|
{
|
||||||
|
Console.WriteLine(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDataStore<T>
|
||||||
|
{
|
||||||
|
void Add(T value);
|
||||||
|
T Get(int index);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataStore<T> : IDataStore<T>
|
||||||
|
{
|
||||||
|
private List<T> values = new List<T>();
|
||||||
|
|
||||||
|
public void Add(T value)
|
||||||
|
{
|
||||||
|
values.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get(int index)
|
||||||
|
{
|
||||||
|
return values[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
理解:
|
||||||
|
1. `DataStore<T>` 是泛型类
|
||||||
|
2. `IDataStore<T>` 是泛型接口
|
||||||
|
3. `Print<T>` 是泛型方法
|
||||||
|
4. `DataStore<int>` 只能保存整数
|
||||||
|
5. `DataStore<string>` 只能保存字符串
|
||||||
|
|
||||||
|
----
|
||||||
|
# 注意
|
||||||
|
|
||||||
|
1. 泛型类型参数常用 `T` 表示,但不是固定写法
|
||||||
|
2. 多个泛型参数常写成 `TKey`、`TValue`、`TItem`
|
||||||
|
3. `List<T>` 比 `ArrayList` 更常用,也更安全
|
||||||
|
4. 泛型在编译期就能检查类型错误
|
||||||
|
5. 使用泛型可以减少 `object` 带来的强制转换
|
||||||
|
6. 如果需要在泛型中 `new T()`,必须加 `where T : new()` 约束
|
||||||
|
7. 泛型不是“任何类型都乱放”,而是“使用时确定一种类型”
|
||||||
|
|
||||||
|
----
|
||||||
|
# 小结
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
泛型就是==把类型当作参数传进去==。
|
||||||
|
|
||||||
|
记忆:
|
||||||
|
普通参数控制“值”。
|
||||||
|
泛型参数控制“类型”。
|
||||||
|
|
||||||
|
最常见写法:
|
||||||
|
```csharp
|
||||||
|
List<int> nums = new List<int>();
|
||||||
|
Dictionary<string, int> scores = new Dictionary<string, int>();
|
||||||
|
```
|
||||||
+16
-13
@@ -20,20 +20,21 @@
|
|||||||
6. [[运算符重载]]
|
6. [[运算符重载]]
|
||||||
7. [[内部类和分部类]]
|
7. [[内部类和分部类]]
|
||||||
8. [[继承]]
|
8. [[继承]]
|
||||||
9. 装箱与拆箱
|
9. [[里氏替换原则]]
|
||||||
10. 密封类
|
10. [[万物之父和装箱与拆箱|装箱与拆箱]]
|
||||||
11. 多态vob
|
11. [[密封类]]
|
||||||
12. 抽象类和抽象函数
|
12. [[多态vob]]
|
||||||
13. 接口
|
13. [[抽象类和抽象方法|抽象类和抽象函数]]
|
||||||
14. 密封函数
|
14. [[接口]]
|
||||||
15. 命名空间
|
15. [[密封函数]]
|
||||||
16. 抽象类的接口的区别
|
16. [[命名空间]]
|
||||||
|
17. [[抽象类和接口的区别|抽象类的接口的区别]]
|
||||||
# 进阶
|
# 进阶
|
||||||
1. ArrayList
|
1. [[ArrayList]]
|
||||||
2. Stack
|
2. [[Stack]]
|
||||||
3. Queue
|
3. [[Queue]]
|
||||||
4. Hashtable
|
4. [[Hashtable]]
|
||||||
5. 泛型和泛型约束
|
5. [[泛型]]和[[泛型约束]]
|
||||||
6. List
|
6. List
|
||||||
7. Dictionary
|
7. Dictionary
|
||||||
8. LinkedList
|
8. LinkedList
|
||||||
@@ -46,3 +47,5 @@
|
|||||||
15. 多线程
|
15. 多线程
|
||||||
16. 预处理器
|
16. 预处理器
|
||||||
17. 反射
|
17. 反射
|
||||||
|
# 杂记
|
||||||
|
1. [[命名法]]
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# Visual Studio 中使用 Git
|
||||||
|
|
||||||
|
这篇记录 Visual Studio 里使用 Git 管理 Obsidian 笔记库的流程。
|
||||||
|
|
||||||
|
当前笔记库路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub 远程仓库:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://github.com/Ki5ter/Obsidian.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## VS 里的几个设置不要混淆
|
||||||
|
|
||||||
|
`默认位置 Default location`:
|
||||||
|
不是当前正在工作的仓库路径。它只是 Visual Studio 以后 clone 新仓库时默认放到哪里。
|
||||||
|
|
||||||
|
推荐设置成类似:
|
||||||
|
|
||||||
|
```text
|
||||||
|
E:\GitRepos
|
||||||
|
```
|
||||||
|
|
||||||
|
不推荐设置成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes
|
||||||
|
```
|
||||||
|
|
||||||
|
原因是 `MyNotes` 本身就是一个仓库,不应该再作为所有新仓库的默认存放目录。
|
||||||
|
|
||||||
|
`Git 全局配置 Git Global Config`:
|
||||||
|
影响电脑上所有 Git 仓库,比如用户名、邮箱、全局代理。
|
||||||
|
|
||||||
|
`Git 仓库配置 Git Repository Config`:
|
||||||
|
只影响当前打开的这个仓库,比如当前仓库的 `origin` 地址。
|
||||||
|
|
||||||
|
`远程库 Remotes`:
|
||||||
|
配置当前本地仓库对应的远程仓库地址。常见写法:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name: origin
|
||||||
|
URL: https://github.com/Ki5ter/Obsidian.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第一次使用的正确流程
|
||||||
|
|
||||||
|
1. 在资源管理器中找到笔记库文件夹:
|
||||||
|
|
||||||
|
```text
|
||||||
|
E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 如果笔记库在 OneDrive 里,先右键文件夹,选择:
|
||||||
|
|
||||||
|
```text
|
||||||
|
始终保留在此设备上
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 用 Visual Studio 打开文件夹:
|
||||||
|
|
||||||
|
```text
|
||||||
|
文件 -> 打开 -> 文件夹
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 打开 `Git 更改` 窗口。
|
||||||
|
|
||||||
|
5. 如果还不是 Git 仓库,选择创建 Git 仓库,或者用命令:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" init
|
||||||
|
```
|
||||||
|
|
||||||
|
6. 在当前仓库的远程库里添加:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name: origin
|
||||||
|
URL: https://github.com/Ki5ter/Obsidian.git
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 在 `Git 更改` 窗口输入提交信息,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Initial Obsidian notes backup
|
||||||
|
```
|
||||||
|
|
||||||
|
8. 点击 `全部提交`。
|
||||||
|
|
||||||
|
9. 点击 `推送`。
|
||||||
|
|
||||||
|
## 提交和推送的区别
|
||||||
|
|
||||||
|
`全部提交`:
|
||||||
|
只是把改动提交到本地仓库。提交成功后,GitHub 上还不会有变化。
|
||||||
|
|
||||||
|
`推送`:
|
||||||
|
把本地提交上传到 GitHub 远程仓库。
|
||||||
|
|
||||||
|
如果 VS 显示:
|
||||||
|
|
||||||
|
```text
|
||||||
|
2 传出
|
||||||
|
```
|
||||||
|
|
||||||
|
意思是本地有 2 个 commit 还没有推送到远程仓库。
|
||||||
|
|
||||||
|
## 本地分支名 master 和 main
|
||||||
|
|
||||||
|
有些仓库默认分支叫 `master`,GitHub 新仓库常见默认分支叫 `main`。
|
||||||
|
|
||||||
|
如果本地当前是 `master`,可以直接推送:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" push -u origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
如果想改成 `main`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" branch -M main
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clash Verge 代理问题
|
||||||
|
|
||||||
|
如果推送时报错:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Failed to connect to 127.0.0.1 port 10809
|
||||||
|
```
|
||||||
|
|
||||||
|
说明 Git 被配置成使用 `127.0.0.1:10809` 代理,但 Clash Verge 没有在这个端口提供服务。
|
||||||
|
|
||||||
|
当前 Clash Verge 配置中常见端口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mixed-port: 7897
|
||||||
|
socks-port: 7898
|
||||||
|
port: 7899
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐让 Git 使用 `mixed-port`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git config --global http.proxy http://127.0.0.1:7897
|
||||||
|
git config --global https.proxy http://127.0.0.1:7897
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" config http.proxy http://127.0.0.1:7897
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" config https.proxy http://127.0.0.1:7897
|
||||||
|
```
|
||||||
|
|
||||||
|
然后再推送:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" push -u origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
或者回 Visual Studio 里点击 `推送`。
|
||||||
|
|
||||||
|
如果不想让 Git 走代理,可以清除代理:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git config --global --unset http.proxy
|
||||||
|
git config --global --unset https.proxy
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" config --unset http.proxy
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" config --unset https.proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
## TUN 虚拟网卡模式
|
||||||
|
|
||||||
|
TUN 或虚拟网卡模式可以让更多系统流量进入代理,理论上也可能让 Git 不需要单独设置代理。
|
||||||
|
|
||||||
|
但它不是解决 Git 推送问题的第一选择,因为它会涉及管理员权限、DNS、路由和规则匹配。
|
||||||
|
|
||||||
|
当前场景更推荐:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Clash Verge 正常运行
|
||||||
|
Git 的 http.proxy / https.proxy 指向 Clash Verge 的 mixed-port
|
||||||
|
VS 中正常点击推送
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见排查命令
|
||||||
|
|
||||||
|
查看当前仓库状态:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" status --short --branch
|
||||||
|
```
|
||||||
|
|
||||||
|
查看远程仓库:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" remote -v
|
||||||
|
```
|
||||||
|
|
||||||
|
查看分支:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" branch -vv
|
||||||
|
```
|
||||||
|
|
||||||
|
查看最近提交:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" log --oneline --decorate -5
|
||||||
|
```
|
||||||
|
|
||||||
|
查看 Git 代理:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git config --global --get-regexp "http.*proxy|https.*proxy"
|
||||||
|
git -C "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes" config --get-regexp "http.*proxy|https.*proxy"
|
||||||
|
```
|
||||||
|
|
||||||
|
## OneDrive 注意事项
|
||||||
|
|
||||||
|
如果出现下面这些错误:
|
||||||
|
|
||||||
|
```text
|
||||||
|
not a git repository
|
||||||
|
Permission denied
|
||||||
|
The cloud file provider is not running
|
||||||
|
unable to access .git/info/exclude
|
||||||
|
```
|
||||||
|
|
||||||
|
优先检查 OneDrive 是否正在运行,以及笔记库是否已经设置为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
始终保留在此设备上
|
||||||
|
```
|
||||||
|
|
||||||
|
Git 仓库里的 `.git` 目录不要只存在云端占位。`.git` 目录读不到时,Visual Studio 会无法刷新仓库设置,也可能无法提交或推送。
|
||||||
|
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
# Git 基础知识
|
||||||
|
|
||||||
|
Git 是一个版本控制工具。它可以记录文件夹在不同时间点的状态,让我们知道改了什么、什么时候改的,也可以把本地内容同步到 GitHub 这样的远程仓库。
|
||||||
|
|
||||||
|
## 三个核心位置
|
||||||
|
|
||||||
|
`工作区 workspace`:
|
||||||
|
你平时直接看到和编辑的真实文件夹,比如 Obsidian 里的 `.md` 笔记、图片、模板等。
|
||||||
|
|
||||||
|
`本地仓库 local repository`:
|
||||||
|
工作区里有一个隐藏目录 `.git` 后,这个文件夹就成为 Git 本地仓库。`.git` 里保存提交历史、分支、远程地址等信息。
|
||||||
|
|
||||||
|
`远程仓库 remote repository`:
|
||||||
|
放在 GitHub、Gitee 等平台上的仓库。比如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://github.com/Ki5ter/Obsidian.git
|
||||||
|
```
|
||||||
|
|
||||||
|
本地仓库和远程仓库不是同一个东西。`commit` 只提交到本地,`push` 才会上传到远程。
|
||||||
|
|
||||||
|
## 常见概念
|
||||||
|
|
||||||
|
`stage/index 暂存区`:
|
||||||
|
准备放进下一次提交的改动。可以理解为“这次 commit 我要带上的内容清单”。
|
||||||
|
|
||||||
|
`commit 提交`:
|
||||||
|
一次本地快照。提交后,Git 会记住当前这批改动。
|
||||||
|
|
||||||
|
`branch 分支`:
|
||||||
|
一条提交历史线。常见主分支名是 `main` 或 `master`。
|
||||||
|
|
||||||
|
`remote/origin`:
|
||||||
|
远程仓库地址的别名。通常把 GitHub 远程仓库命名为 `origin`。
|
||||||
|
|
||||||
|
`push 推送`:
|
||||||
|
把本地 commit 上传到远程仓库。
|
||||||
|
|
||||||
|
`pull 拉取`:
|
||||||
|
从远程仓库下载最新改动,并合并到当前本地分支。
|
||||||
|
|
||||||
|
`fetch 获取`:
|
||||||
|
只下载远程仓库信息,不自动合并。
|
||||||
|
|
||||||
|
## 基本工作流程
|
||||||
|
|
||||||
|
1. 打开工作区,也就是要纳入版本控制的文件夹。
|
||||||
|
2. 执行 `git init`,把文件夹初始化成本地 Git 仓库。
|
||||||
|
3. 配置远程仓库,也就是添加 `origin`。
|
||||||
|
4. 修改、新增或删除文件。
|
||||||
|
5. 把改动加入暂存区。
|
||||||
|
6. 提交到本地仓库。
|
||||||
|
7. 推送到远程仓库。
|
||||||
|
|
||||||
|
对应命令:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd "E:\Obsidian\OneDrive\Apps\remotely-save\MyNotes"
|
||||||
|
git init
|
||||||
|
git remote add origin https://github.com/Ki5ter/Obsidian.git
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial Obsidian notes backup"
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
如果当前分支叫 `master`,推送命令也可以是:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git push -u origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
## main 和 master
|
||||||
|
|
||||||
|
`main` 和 `master` 都可以作为主分支名。
|
||||||
|
|
||||||
|
现在 GitHub 新仓库通常默认使用 `main`,旧仓库或某些工具可能默认使用 `master`。
|
||||||
|
|
||||||
|
把当前分支改名为 `main`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git branch -M main
|
||||||
|
```
|
||||||
|
|
||||||
|
第一次推送并设置上游分支:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
`-u` 的意思是把本地分支和远程分支关联起来。设置过一次后,以后可以直接使用:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git push
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常用检查命令
|
||||||
|
|
||||||
|
查看当前仓库状态:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
查看当前分支:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git branch
|
||||||
|
```
|
||||||
|
|
||||||
|
查看本地分支和远程分支关系:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git branch -vv
|
||||||
|
```
|
||||||
|
|
||||||
|
查看远程仓库地址:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git remote -v
|
||||||
|
```
|
||||||
|
|
||||||
|
查看提交历史:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git log --oneline --decorate -5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Obsidian 笔记库使用 Git 的注意事项
|
||||||
|
|
||||||
|
Obsidian 笔记库适合用 Git 备份,因为大多数内容都是 Markdown 文本文件。
|
||||||
|
|
||||||
|
建议提交:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.md 笔记
|
||||||
|
图片、附件
|
||||||
|
模板
|
||||||
|
必要的 .obsidian 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
谨慎提交:
|
||||||
|
|
||||||
|
```text
|
||||||
|
密码、密钥、Token
|
||||||
|
个人隐私内容
|
||||||
|
插件缓存
|
||||||
|
特别大的二进制文件
|
||||||
|
```
|
||||||
|
|
||||||
|
如果笔记库在 OneDrive 里,要注意 `.git` 目录必须真实保留在本机。否则 Git 或 Visual Studio 可能会报错,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
not a git repository
|
||||||
|
Permission denied
|
||||||
|
The cloud file provider is not running
|
||||||
|
```
|
||||||
|
|
||||||
|
处理方式是在资源管理器里右键笔记库文件夹,选择:
|
||||||
|
|
||||||
|
```text
|
||||||
|
始终保留在此设备上
|
||||||
|
```
|
||||||
|
|
||||||
|
重点是整个笔记库和 `.git` 目录都要保留在本机。
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
command \[-options] \[parameter]
|
||||||
|
- command:命令本身
|
||||||
|
- -options:可选非必填,一些选项,控制命令细节
|
||||||
|
- parameter:可选非必填的命令参数 用于命令指向的目标
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- ls -l /home/* 以列表的形式,显示/home/* 目录下所有的文件
|
||||||
|
- cp -r test1 test2 赋值文件夹test1为test2
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Windows是树形结构,且分为盘符
|
||||||
|
但是Linux没有盘符,只存在一个目录`/`下
|
||||||
Reference in New Issue
Block a user